关于c#:如何在不同的CPU内核上生成线程?

How do I spawn threads on different CPU cores?

假设我有一个使用C#编写的程序,该程序的计算量很大,例如将WAV文件列表编码为MP3。 通常,我会一次对文件进行编码,但比方说,我希望程序计算出我拥有多少个CPU内核,并在每个内核上增加一个编码线程。 因此,当我在四核CPU上运行程序时,程序会确定它是四核CPU,计算出有四个内核可以使用,然后产生四个用于编码的线程,每个线程都在自己的单独线程上运行 中央处理器。 我该怎么做?

如果内核分散在多个物理CPU上,这会有什么不同吗? 例如,如果我有一台装有两个四核CPU的机器,是否有任何特殊考虑,或者两个模具中的八个内核在Windows中是否被认为是相等的?


别那么做。

而是使用线程池。线程池是框架的一种机制(实际上是一个类),您可以查询新线程。

当您请求一个新线程时,它将为您提供一个新线程或使工作排队,直到释放线程为止。这样,框架负责决定是否应根据当前CPU的数量创建更多线程。

编辑:此外,正如已经提到的,操作系统负责在不同的CPU之间分配线程。


它不一定像使用线程池那样简单。

默认情况下,线程池为每个CPU分配多个线程。由于涉及到您正在执行的工作的每个线程都具有成本(任务切换开销,CPU的非常有限的L1,L2和L3高速缓存的使用等),因此,要使用的最佳线程数为<=可用CPU的数量-除非每个线程都向其他计算机请求服务-诸如高度可扩展的Web服务。在某些情况下,尤其是那些涉及硬盘读写比CPU活动更多的情况,使用1个线程实际上要比使用多个线程更好。

对于大多数应用程序,当然对于WAV和MP3编码,应该将辅助线程的数量限制为可用CPU的数量。这是一些C#代码来查找CPU的数量:

1
2
3
4
int processors = 1;
string processorsStr = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS");
if (processorsStr != null)
    processors = int.Parse(processorsStr);

不幸的是,这并不像限制CPU数量那么简单。您还必须考虑硬盘控制器和磁盘的性能。

真正找到最佳线程数的唯一方法是尝试错误。当您使用硬盘,Web服务等时,尤其如此。使用硬盘时,最好不要在四核处理器CPU上使用所有四个处理器。另一方面,对于某些Web服务,您可能最好每个CPU发出10个甚至100个请求。


对于托管线程,执行此操作的复杂度要比本地线程高。这是因为CLR线程没有直接绑定到本机OS线程。换句话说,CLR可以根据需要将托管线程从本机线程切换到本机线程。提供函数Thread.BeginThreadAffinity可以将托管线程与本机OS线程锁定在一起。到那时,您可以尝试使用本机API来赋予基础本机线程处理器亲和力。正如每个人在这里建议的那样,这不是一个好主意。实际上,有文档表明,如果线程仅限于单个处理器或内核,则可以减少处理时间。

您还可以浏览System.Diagnostics.Process类。在这里,您可以找到一个函数来枚举ProcessThread对象作为ProcessThread对象的集合。此类具有设置ProcessorAffinity或什至设置首选处理器的方法-不确定是什么。

免责声明:我曾经遇到过类似的问题,我认为CPU的利用率不高,并对此进行了大量研究。但是,根据我阅读的所有内容,似乎也不是一个好主意,正如此处发布的评论所证明的那样。但是,它仍然很有趣,并且可以进行实验学习。


尽管我同意这里的大多数答案,但我认为值得添加新的考虑因素:Speedstep技术。

当在多核系统上运行CPU密集型单线程作业时(在我的情况下,它是在Windows Server 2012下具有6个真实核心(12个带有HT的)的Xeon E5-2430),该作业分散在所有12个核心中,使用每个核心的约8.33%,并且永远不会触发速度提升。 CPU保持在1.2 GHz。

当我将线程关联性设置为特定的内核时,它使用了该内核的100%左右,从而导致CPU在2.5 GHz时最大输出,使性能提高了一倍以上。

这是我使用的程序,它只是循环增加变量。当使用-a调用时,它将亲和力设置为核心1。亲和力部分基于该文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace Esquenta
{
    class Program
    {
        private static int numThreads = 1;
        static bool affinity = false;
        static void Main(string[] args)
        {
            if (args.Contains("-a"))
            {
                affinity = true;
            }
            if (args.Length < 1 || !int.TryParse(args[0], out numThreads))
            {
                numThreads = 1;
            }
            Console.WriteLine("numThreads:" + numThreads);
            for (int j = 0; j < numThreads; j++)
            {
                var param = new ParameterizedThreadStart(EsquentaP);
                var thread = new Thread(param);
                thread.Start(j);
            }

        }

        static void EsquentaP(object numero_obj)
        {
            int i = 0;
            DateTime ultimo = DateTime.Now;
            if(affinity)
            {
                Thread.BeginThreadAffinity();
                CurrentThread.ProcessorAffinity = new IntPtr(1);
            }
            try
            {
                while (true)
                {
                    i++;
                    if (i == int.MaxValue)
                    {
                        i = 0;
                        var lps = int.MaxValue / (DateTime.Now - ultimo).TotalSeconds / 1000000;
                        Console.WriteLine("Thread" + numero_obj +"" + lps.ToString("0.000") +" M loops/s");
                        ultimo = DateTime.Now;
                    }
                }
            }
            finally
            {
                Thread.EndThreadAffinity();
            }
        }

        [DllImport("kernel32.dll")]
        public static extern int GetCurrentThreadId();

        [DllImport("kernel32.dll")]
        public static extern int GetCurrentProcessorNumber();
        private static ProcessThread CurrentThread
        {
            get
            {
                int id = GetCurrentThreadId();
                return Process.GetCurrentProcess().Threads.Cast<ProcessThread>().Single(x => x.Id == id);
            }
        }
    }
}

结果:

results

如任务管理器所示,处理器速度类似于CPU-Z报告的速度:

enter image description here


您绝对可以通过在程序内部编写例程来完成此操作。

但是,您不应该尝试这样做,因为操作系统是管理这些内容的最佳人选。我的意思是用户模式程序不应尝试这样做。

但是,有时可以做到(对于真正的高级用户),以实现负载平衡,甚至找出真正的多线程多核问题(数据竞速/缓存一致性...),因为不同的线程将真正在不同的处理器上执行。

话虽如此,如果您仍然想要实现,我们可以通过以下方式实现。我正在为您提供(Windows OS)的伪代码,但是它们也可以在Linux上轻松完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define MAX_CORE 256
processor_mask[MAX_CORE] = {0};
core_number = 0;

Call GetLogicalProcessorInformation();
// From Here we calculate the core_number and also we populate the process_mask[] array
// which would be used later on to set to run different threads on different CORES.


for(j = 0; j < THREAD_POOL_SIZE; j++)
Call SetThreadAffinityMask(hThread[j],processor_mask[j]);
//hThread is the array of handles of thread.
//Now if your number of threads are higher than the actual number of cores,
// you can use reset the counters(j) once you reach to the"core_number".

在调用上述例程之后,线程将始终以以下方式执行:

1
2
3
4
5
6
7
8
9
10
11
12
Thread1-> Core1
Thread2-> Core2
Thread3-> Core3
Thread4-> Core4
Thread5-> Core5
Thread6-> Core6
Thread7-> Core7
Thread8-> Core8

Thread9-> Core1
Thread10-> Core2
...............

有关更多信息,请参阅手册/ MSDN以了解有关这些概念的更多信息。


您不必担心自己这样做。我有在双四核计算机上运行的多线程.NET应用程序,无论线程是如何启动的(无论是通过ThreadPool还是手动启动),我都能在所有内核上看到均匀的工作分配。


您无法执行此操作,因为只有操作系统才能执行此操作。如果您决定的话.....那么将很难编写应用程序代码。因为那样,您还需要注意处理器间的通信。关键部分。对于每个应用程序,您都必须创建自己的信号灯或互斥锁……操作系统自己可以对它们提供通用的解决方案。


在不同内核之间拆分线程是操作系统的工作,当线程使用大量CPU时间时,它将自动执行。不用担心至于要找出您的用户有多少个内核,请在C#中尝试Environment.ProcessorCount


每个线程通常由操作系统本身处理...因此在一个4核系统上生成4个线程,操作系统将决定要在哪个内核上运行,每个内核通常是1个线程。


您不应该(如上所述)尝试自己分配这类内容的原因之一是,您只是没有足够的信息来正确地进行处理,尤其是在NUMA等方面。

如果您有一个要运行的线程,并且有一个内核处于空闲状态,则内核将运行您的线程,请不要担心。


推荐阅读