如何在不同的CPU核心上生成线程?

69
假设我有一个用C#编写的程序,它执行一些计算密集型操作,例如将WAV文件列表编码为MP3。通常情况下,我会逐个文件地进行编码,但是假设我想让程序找出我有多少个CPU核心并在每个核心上启动一个编码线程。因此,当我在四核CPU上运行程序时,程序会确定它是四核CPU,确定有四个核心可供使用,然后为编码生成四个线程,每个线程在其自己独立的CPU上运行。我该如何做到这一点?如果这些核心分布在多个物理CPU上,这是否有所不同?也就是说,如果我的计算机上有两个四核CPU,是否有任何特殊考虑,或者在Windows中,这两个芯片中的八个核心被认为是相等的?
10个回答

64

不要费力地这样做。

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

当您请求一个新线程时,它会给您一个新线程或将工作排队直到有空闲线程。这样,框架负责决定是否应该根据当前CPU数量创建更多线程。

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


67
这是一个与.NET相关的问题。为什么你会没有.NET? - Dave Van den Eynde
更进一步,TPL(任务并行)的包含也承担了这项工作,您可以在此处阅读更多信息:http://msdn.microsoft.com/en-us/magazine/cc163340.aspx - Usman Masood

18

使用线程池并不一定简单。

默认情况下,线程池为每个CPU分配多个线程。由于参与你所做工作的每个线程都有成本(任务切换开销、使用CPU非常有限的L1、L2甚至L3缓存等),因此使用的最佳线程数<=可用CPU数量,除非每个线程都在请求其他机器提供的服务-例如高度可伸缩的Web服务。对于某些情况,尤其是涉及更多硬盘读写而非CPU活动的情况,一个线程可能比多个线程更好。

对于大多数应用程序,特别是WAV和MP3编码,应将工作线程数限制为可用CPU数。以下是一些C#代码,用于查找CPU数量:

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

很遗憾,仅仅限制CPU核数是不够的。您还需要考虑硬盘控制器和磁盘的性能。

找到最优线程数量的唯一方法是通过试验和错误。当使用硬盘、Web服务等时,这一点尤为真实。对于硬盘而言,您可能最好不要使用四个处理器内所有的处理器。另一方面,在某些Web服务中,每个CPU进行10甚至100个请求可能更好。


5
最佳线程数量略多于CPU数量。您的相反观点是错误的。如果线程不能继续前进而发生任务切换,无论您创建了多少线程,都会出现该任务切换。由于操作系统会谨慎选择时间片以确保最大化使用时间片,因此从完全利用时间片导致的任务切换可以忽略不计。 - David Schwartz

14
虽然我同意这里大部分的答案,但我认为还值得添加一个新的考虑因素:Speedstep技术。
当在多核系统上运行一个CPU密集型的单线程作业时,在我的情况下是在带有6个实际核心(12个超线程)的Xeon E5-2430下运行Windows Server 2012时,该作业被分散到所有12个内核中,每个内核使用约8.33%,从未触发速度增加。 CPU保持在1.2 GHz。
当我将线程亲和力设置为特定核心时,它使用了该核心的大约100%,导致CPU最高峰达到2.5 GHz,性能增加了一倍以上。
这是我使用的程序,它只是循环递增一个变量。当使用-a调用时,它将将亲和力设置为内核1。亲和力部分基于此文章
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


感谢您提供有关CPU使用的宝贵信息。我将尝试使用您的代码来满足我的需求。 - Pawel Cioch
在这两种情况下,我在iCore-7上获得了大约550M次循环。控制台应用程序默认为单线程。我们可能需要使用https://learn.microsoft.com/en-us/dotnet/api/system.stathreadattribute?view=netframework-4.7.2 - Pawel Cioch
在这种情况下,我故意只运行一个线程,以查看相同的线程如何分布到不同的核心。要在您的PC上运行此测试,您必须关闭所有CPU密集型应用程序,例如Visual Studio和浏览器,直到速度稳定在较低值。然后,当您使用亲和力运行它时,您应该在任务管理器中看到速度增加。 - AlexDev
是的,在我分析了这个应用程序之后,我看到了它的目的,非常酷的东西,一切都很顺畅。是的,我能够将1个核心的负载达到100%,或者将负载分散到所有核心上。谢谢。 - Pawel Cioch

9
对于托管线程而言,实现这一点的复杂度比本地线程高一个级别。这是因为CLR线程并非直接绑定到本地操作系统线程。换句话说,CLR可以根据需要将托管线程从一个本地线程切换到另一个本地线程。提供了Thread.BeginThreadAffinity函数来将托管线程与本地操作系统线程锁定在一起。此时,您可以尝试使用本机API来给底层本地线程处理器分配亲和性。正如所有人在这里建议的那样,这不是一个很好的主意。事实上,有文档表明,如果限制在单个处理器或核心上,线程可能会接收更少的处理时间。
你还可以探索System.Diagnostics.Process类。在那里,你可以找到一个函数来枚举一个进程的线程作为ProcessThread对象的集合。这个类有方法来设置ProcessorAffinity甚至设置一个首选处理器 - 不确定这是什么。免责声明:我遇到过类似的问题,认为CPU利用率很低,并研究了很多这方面的东西; 然而,根据我所读到的所有内容,这似乎不是一个非常好的主意,正如这里发布的评论所证明的那样。然而,实验仍然是有趣和学习的经验。

6
你可以通过在程序内部编写例程来实现这一点。
然而,你不应该尝试这样做,因为操作系统是管理这些内容的最佳候选者。我的意思是用户模式程序不应该尝试这样做。
然而,有时(对于真正高级的用户),可以通过实现负载平衡甚至发现真正的多线程多核问题(数据竞争/缓存一致性等),因为不同的线程将真正在不同的处理器上执行。
话虽如此,如果你仍然想要实现,我们可以按照以下方式进行。我提供了伪代码(Windows操作系统),但在Linux上也可以轻松完成。
#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".

在调用上述例程后,线程将始终按以下方式执行:
Thread1-> Core1
Thread2-> Core2
Thread3-> Core3
Thread4-> Core4
Thread5-> Core5
Thread6-> Core6
Thread7-> Core7
Thread8-> Core8

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

更多信息,请参考手册/MSDN以了解这些概念。


3

您不需要自己担心这个问题。我有在双四核机器上运行的多线程.NET应用程序,无论是通过线程池还是手动启动线程,我都可以看到工作在所有核心上的良好均匀分布。


2

通常情况下,每个线程运行在哪个核心上由操作系统自己处理...因此,在一个四核系统上生成四个线程时,操作系统将决定在哪些核心上运行每个线程,通常是每个核心上运行一个线程。


2

操作系统的工作是将线程分配到不同的核心上,当您的线程使用大量CPU时间时,它会自动执行。不用担心这个问题。至于如何找出用户有多少个核心,请尝试在C#中使用Environment.ProcessorCount


2

您不能这样做,因为只有操作系统才有特权执行此操作。如果您决定这样做......那么编写应用程序将变得困难。因为您还需要关注处理器间通信、临界区等问题。对于每个应用程序,您都需要创建自己的信号量或互斥锁......而操作系统会通过自身来提供一个通用解决方案。


1

不要(正如所说的)尝试自己分配这种类型的东西的原因之一是,您没有足够的信息来正确地执行它,特别是在 NUMA 等未来的情况下。

如果您有一个准备好运行的线程,并且有一个空闲的内核,内核将运行您的线程,不必担心。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接