深度解析线程池ThreadPool实现原理
提到线程池,很多同学对这个概念比较熟悉,实际的项目中也会经常使用,但是可能对于其内部的实现原理和使用的场景和优化建议不是很熟悉,这次我们就来一起看看线程池在实际的实现过程的逻辑和原理吧。线程池(ThreadPool)是一个重要的执行引擎,用于管理应用程序中的多线程工作。它负责调度和执行应用程序提交的工作项(Work Item),这些工作项可以是委托或其他表示可执行代码的结构。
关于 .NET 线程池的一些重要特点:
(1)、线程重用:线程池在应用程序生命周期内维护一组可重用的线程。而不是为每个工作项创建新的线程,线程池会重用现有线程,从而减少了创建和销毁线程的开销。
(2)、线程数量控制:线程池可以根据负载动态调整线程数量,以适应应用程序的需求。线程池会在启动时创建一组初始线程,并根据需要动态添加或移除线程,以确保工作项得到及时处理并避免资源浪费。
(3)、工作项队列:线程池通过工作项队列来管理待处理的工作项。当线程池中的线程处于空闲状态时,它们会从队列中获取工作项并执行。工作项队列采用先进先出(FIFO)的原则,确保工作项按照提交的顺序执行。
(4)、线程安全性:线程池的操作是线程安全的,因此可以从多个线程同时提交工作项而不会导致竞态条件或数据损坏。
(5)、性能优化:线程池具有一些性能优化机制,例如线程池工作者线程的自动终止和重新启动、线程的空闲线程清理等,以提高整体性能和资源利用率。
(6)、配置选项:开发人员可以通过配置文件或编程方式对线程池进行一些基本设置,例如最小和最大线程数、线程空闲时间等。
接下来我们还是以一个简单的样例开始我们今天对于线程池的介绍吧。
1 using System; 2 using System.Threading; 3 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 // 提交一些工作项到线程池 9 for (int i = 0; i < 5; i++) 10 { 11 int taskNumber = i + 1; 12 ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork), taskNumber); 13 } 14 15 Console.WriteLine("工作项已提交到线程池。"); 16 17 // 等待一段时间以便观察输出 18 Thread.Sleep(2000); 19 20 Console.WriteLine("主线程终止。"); 21 } 22 23 static void DoWork(object data) 24 { 25 int taskNumber = (int)data; 26 Console.WriteLine("工作项 {0} 开始执行,线程ID:{1}", taskNumber, Thread.CurrentThread.ManagedThreadId); 27 28 // 模拟工作项执行 29 Thread.Sleep(1000); 30 31 Console.WriteLine("工作项 {0} 执行完成,线程ID:{1}", taskNumber, Thread.CurrentThread.ManagedThreadId); 32 } 33 }
由上面的线程池应用样例,我们发现线程池的核心类和操作方法是ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork), taskNumber);那我们就接着看看对应的实现逻辑。ThreadPool.QueueUserWorkItem 方法用于将工作项提交到线程池中。提交了五个工作项到线程池,并在主线程中等待一段时间,以便观察工作项的执行情况。每个工作项的执行都是由线程池中的线程来完成的,而不需要我们显式地创建和管理线程。
1 internal static readonly ThreadPoolWorkQueue s_workQueue = new ThreadPoolWorkQueue(); 2 3 public static bool QueueUserWorkItem(WaitCallback callBack, object? state) 4 { 5 ExecutionContext? context = ExecutionContext.Capture(); 6 7 object tpcallBack = (context == null || context.IsDefault) ? 8 new QueueUserWorkItemCallbackDefaultContext(callBack!, state) : 9 (object)new QueueUserWorkItemCallback(callBack!, state, context); 10 11 s_workQueue.Enqueue(tpcallBack, forceGlobal: true); 12 13 return true; 14 }
ExecutionContext.Capture()捕获当前的执行上下文,包括当前的线程的安全上下文信息。ExecutionContext 表示执行环境,包含了当前线程的安全上下文和异步状态。将 tpcallBack 对象添加到工作队列中,将工作项添加到队列中, //forceGlobal: true 可能是一个参数,用于指示是否强制将工作项添加到全局队列中。接下来我们来看ThreadPoolWorkQueue的实现逻辑。
1 private const int ProcessorsPerAssignableWorkItemQueue = 16; 2 3 private static readonly int s_assignableWorkItemQueueCount = 4 Environment.ProcessorCount <= 32 ? 0 : 5 (Environment.ProcessorCount + (ProcessorsPerAssignableWorkItemQueue - 1)) / ProcessorsPerAssignableWorkItemQueue; 6 7 internal readonly ConcurrentQueue<object>[] _assignableWorkItemQueues = new ConcurrentQueue<object>[s_assignableWorkItemQueueCount]; 8 9 private readonly int[] _assignedWorkItemQueueThreadCounts = s_assignableWorkItemQueueCount > 0 ? new int[s_assignableWorkItemQueueCount] : Array.Empty<int>(); 10 12 public ThreadPoolWorkQueue() 13 { 14 for (int i = 0; i < s_assignableWorkItemQueueCount; i++) 15 { 16 _assignableWorkItemQueues[i] = new ConcurrentQueue<object>(); 17 } 18 } 19 21 public void Enqueue(object callback, bool forceGlobal) 22 { 23 ThreadPoolWorkQueueThreadLocals? tl; 24 25 if (!forceGlobal && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null) 26 { 27 tl.workStealingQueue.LocalPush(callback); 28 } 29 else 30 { 31 ConcurrentQueue<object> queue = 32 s_assignableWorkItemQueueCount > 0 && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null 33 ? tl.assignedGlobalWorkItemQueue 34 : workItems; 35 queue.Enqueue(callback); 36 } 37 38 EnsureThreadRequested(); 39 } 40 41 42 internal void EnsureThreadRequested() 43 { 44 if (Interlocked.CompareExchange(ref _separated.hasOutstandingThreadRequest, 1, 0) == 0) 45 { 46 ThreadPool.RequestWorkerThread(); 47 } 48 }
ProcessorsPerAssignableWorkItemQueue = 16 为了在决定是否创建可分配工作项队列时提供一种合理的基准。这个值是为了平衡线程池的性能和资源利用率而设置的。通常情况下,一个处理器(或者核心)可以处理多个线程。过多的线程可能会导致线程竞争和资源竞争,从而降低性能。因此,为了避免过度并行化,一个合适的值应该考虑处理器的数量和处理器的处理能力。在这种情况下,当处理器数量超过 32 时,可分配工作项队列会被创建。每个队列代表一组处理器,这样可以在一组处理器中更好地管理线程的并发性和性能。将每组处理器设置为 16 个是一种合理的折衷,可以有效地平衡并行性和资源利用率。
s_assignableWorkItemQueueCount表示可分配工作项队列的数量。 // 根据处理器数量,决定是否创建可分配工作项队列。如果处理器数量小于等于32,则不创建可分配工作项队列, // 否则根据处理器数量和ProcessorsPerAssignableWorkItemQueue计算可分配工作项队列的数量。Environment.ProcessorCount <= 32 以 32 为界限的设定可能是出于性能和资源利用的考量,以及对系统硬件和软件的一般假设。这个界限的设定可能考虑了以下因素:
1、处理器数量:32 通常是一个合理的处理器数量上限。在大多数系统中,32 个处理器已经属于较大规模的多处理器系统。超过 32 个处理器的系统可能是比较罕见的,尤其是在一般的桌面和服务器环境中。
2、并行性能:超过 32 个处理器的系统可能需要更高级别的并行处理和管理。为了避免过度并行化和资源竞争,需要对线程池的行为进行更加精细的控制和调整。
3、资源利用:为每个处理器创建一个可分配的工作项队列需要额外的资源,包括内存和线程管理开销。超过 32 个处理器时,为每个处理器创建队列可能会导致资源浪费和性能下降。
4、系统负载:超过 32 个处理器时,系统的负载可能会更加复杂和不可预测。为了避免过度并行化和负载不均衡,需要更加谨慎地管理线程池的行为。
_assignableWorkItemQueues:并发队列数组,用于存储可分配的工作项。_assignedWorkItemQueueThreadCounts:整型数组,用于跟踪每个可分配工作项队列中的线程数量。Enqueue(object callback, bool forceGlobal):用于向工作队列中添加工作项。如果不强制使用全局队列,并且当前线程存在本地工作队列(ThreadPoolWorkQueueThreadLocals.threadLocals 不为空),则将工作项添加到本地工作队列中。否则,根据是否有可分配工作项队列和当前线程的工作队列,将工作项添加到相应的队列中。 添加工作项后,调用 EnsureThreadRequested 方法来确保有工作线程处于等待状态。EnsureThreadRequested():用于请求一个工作线程来执行工作项。它使用原子操作 Interlocked.CompareExchange 来确保只有一个线程能够请求工作线程,以避免过度并行化。如果当前没有线程请求工作线程,则调用 ThreadPool.RequestWorkerThread() 请求一个工作线程。
由于ThreadPool.RequestWorkerThread()在不同的操作小有不同的实现逻辑,这里我们以Unix内核的系统为例介绍后面的实现逻辑,PortableThreadPool:负责线程池的初始化和工作线程管理。它通过读取配置或使用默认值来初始化线程池的最小和最大工作线程数,并提供了请求工作线程的方法来管理和调度线程池中的工作线程。
1 public static readonly PortableThreadPool ThreadPoolInstance = new PortableThreadPool(); 2 3 //表示强制的最小工作线程数和最大工作线程数。这些值可以通过配置文件进行设置,如果未配置,则默认为 0。 4 private static bool HasForcedMinThreads => 5 ForcedMinWorkerThreads > 0 && (ForcedMaxWorkerThreads <= 0 || ForcedMinWorkerThreads <= ForcedMaxWorkerThreads); 6 private static bool HasForcedMaxThreads => 7 ForcedMaxWorkerThreads > 0 && (ForcedMinWorkerThreads <= 0 || ForcedMinWorkerThreads <= ForcedMaxWorkerThreads); 8 9 private static readonly short ForcedMinWorkerThreads = 10 AppContextConfigHelper.GetInt16Config("System.Threading.ThreadPool.MinThreads", 0, false); 11 12 private static readonly short ForcedMaxWorkerThreads = 13 AppContextConfigHelper.GetInt16Config("System.Threading.ThreadPool.MaxThreads", 0, false); 14 16 private PortableThreadPool() 17 { 18 // 如果配置了强制的最小工作线程数,则使用配置值;否则,默认为处理器的数量(Environment.ProcessorCount)。 19 _minThreads = HasForcedMinThreads ? ForcedMinWorkerThreads : (short)Environment.ProcessorCount; 20 21 if (_minThreads > MaxPossibleThreadCount) 22 { 23 _minThreads = MaxPossibleThreadCount; 24 } 25 26 // 如果配置了强制的最大工作线程数,则使用配置值;否则,默认为 27 // DefaultMaxWorkerThreadCount。在设置最大工作线程数时,确保不超过系统能够支持的最大线程数( 28 // MaxPossibleThreadCount),并且最大线程数不小于最小线程数。 29 _maxThreads = HasForcedMaxThreads ? ForcedMaxWorkerThreads : DefaultMaxWorkerThreadCount; 30 31 if (_maxThreads > MaxPossibleThreadCount) 32 { 33 _maxThreads = MaxPossibleThreadCount; 34 } 35 else if (_maxThreads < _minThreads) 36 { 37 _maxThreads = _minThreads; 38 } 39 } 40 44 internal void RequestWorker() 45 { 46 //方法是一个原子操作,用于增加指定变量的值,并且确保此操作是线程安全的。增加请求工作线程的数量。 47 Interlocked.Increment(ref _separated.numRequestedWorkers); 48 49 //用于将当前对象(可能是当前线程池的一个工作线程)添加到工作中的工作线程列表中。 50 //这可能是为了将当前线程纳入线程池的管理范围内,使其可以执行任务或者进行其他操作。 51 WorkerThread.MaybeAddWorkingWorker(this); 52 53 //用于确保当前线程池的一个门控线程正在运行。 54 //这可能是为了确保线程池中有一个线程处于运行状态,可以接受新的工作任务并调度执行。 55 GateThread.EnsureRunning(this); 56 }
对于PortableThreadPool中的核心代码逻辑已经做了解释,PortableThreadPool():初始化线程池的最小工作线程数和最大工作线程数。RequestWorker():请求线程池中的一个工作线程来执行任务。它通过原子操作增加了请求的工作线程数量,然后将当前线程(可能是工作线程)添加到工作中的工作线程列表中,并确保线程池中有一个门控线程正在运行,以便接受新的任务并调度执行。
这里我们重点看一下RequestWorker()方法的实现逻辑,首先看一下Interlocked.Increment(ref _separated.numRequestedWorkers)的核心逻辑。Interlocked:提供了一种线程安全的方式来修改整数值,保证了在多线程环境中的原子性和线程安全性。通过原子操作,可以避免多个线程同时修改同一个整数值时发生竞态条件和数据不一致的问题。原子操作的实现有多种方式:
1、处理器指令:现代的处理器通常支持原子操作指令,例如 LOCK CMPXCHG、LOCK INC、LOCK DEC 等。这些指令可以确保对内存位置的读取、修改和存储操作是原子的。
2、操作系统提供的原子操作函数:操作系统通常提供了一些原子操作函数,例如 Windows 上的 Interlocked 系列函数。这些函数会调用处理器指令来执行原子操作,确保在多线程环境中的原子性。
InterlockedCompareExchange(ref long location1, long value, long comparand): 1. 重复执行以下步骤,直到操作成功为止: 1.1 读取 location1 的当前值到寄存器 oldValue 中。 1.2 如果 oldValue 不等于 comparand,则退出循环,返回 oldValue。 1.3 否则,将 value 存储到 location1,条件是 location1 的当前值仍然等于 comparand。 -- 使用 LOCK CMPXCHG 指令来实现原子性 1.4 LOCK CMPXCHG [location1], value 2. 返回 oldValue
MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance):确保线程池中的正在处理工作的线程数量与目标数保持一致,以实现线程池的自动调节和管理。对应的核心逻辑已经在代码中进行注释。
internal static void MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance) { //从 threadPoolInstance 中获取线程池实例的计数信息。 ThreadCounts counts = threadPoolInstance._separated.counts; short numExistingThreads, numProcessingWork, newNumExistingThreads, newNumProcessingWork; // 在一个无限循环中,不断检查当前正在处理工作的线程数是否已经达到了目标数。 // 如果当前正在处理工作的线程数已经达到或超过了目标数,那么说明不需要再添加工作线程,直接返回。 while (true) { // 如果当前正在处理工作的线程数还未达到目标数,则尝试更新线程计数。 numProcessingWork = counts.NumProcessingWork; if (numProcessingWork >= counts.NumThreadsGoal) { return; } // 计算新的正在处理工作的线程数和新的线程池中存在的线程数。 newNumProcessingWork = (short)(numProcessingWork + 1); numExistingThreads = counts.NumExistingThreads; newNumExistingThreads = Math.Max(numExistingThreads, newNumProcessingWork); ThreadCounts newCounts = counts; newCounts.NumProcessingWork = newNumProcessingWork; newCounts.NumExistingThreads = newNumExistingThreads; // 尝试将新的计数信息更新到线程池中。如果更新成功,跳出循环;否则,重新获取最新的计数信息。 ThreadCounts oldCounts = threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); if (oldCounts == counts) { break; } counts = oldCounts; } // 计算需要创建的线程数(即新的线程池中存在的线程数与之前的线程池中存在的线程数的差值) int toCreate = newNumExistingThreads - numExistingThreads; // 计算需要释放的线程数(即新的正在处理工作的线程数与之前的正在处理工作的线程数的差值) int toRelease = newNumProcessingWork - numProcessingWork; // 如果有需要释放的线程,调用信号量(s_semaphore)来释放相应数量的线程资源。 if (toRelease > 0) { s_semaphore.Release(toRelease); } // 如果需要创建新的工作线程,就在循环中创建新的工作线程,直到达到所需数量。 while (toCreate > 0) { //使用 new Thread() 创建了一个新的线程对象 CreateWorkerThread(); toCreate--; } }
最后我们再来看一下PortableThreadPool.GateThread()方法的实现逻辑,该方法用于确保门控线程(Gate Thread)在需要时运行。门控线程在线程池中起着重要的作用,它负责接收新的任务并将其分配给空闲的工作线程。门控线程(Gate Thread)是线程池中的一种特殊线程,其作用是接收新的任务并将其分配给空闲的工作线程。通过动态地管理工作线程的数量和任务的调度,确保了线程池的高效运行和任务的及时执行。其主要作用如下:
1、任务调度:门控线程负责接收线程池中的新任务,并决定将任务分配给哪个空闲的工作线程来执行。这样可以确保任务得到及时的执行,并且不会因为工作线程的繁忙而被延迟。
2、工作线程管理:门控线程还负责管理工作线程的数量。它根据任务的负载情况动态地增加或减少工作线程的数量,以确保任务能够及时得到处理,同时避免资源的浪费。
3、线程池健康监控:门控线程还可以监控线程池的健康状况。它可以根据线程池的负载情况和性能指标,动态地调整工作线程的数量,以优化线程池的性能和资源利用率。
// 定义了门控线程的最大运行次数 private const int MaxRuns = 2; // 定义一个掩码,用于表示门控线程的运行状态 private const int GateThreadRunningMask = 0x4; // 确保门控线程正在运行,首先检查门控线程的运行状态,如果发现门控线程尚未运行,就调用EnsureRunningSlow 方法来确保门控线程的运行。 internal static void EnsureRunning(PortableThreadPool threadPoolInstance) { // 判断门控线程是否运行,根据运行次数生成门控线程的运行状态。 if (threadPoolInstance._separated.gateThreadRunningState != GetRunningStateForNumRuns(MaxRuns)) { EnsureRunningSlow(threadPoolInstance); } } internal static void EnsureRunningSlow(PortableThreadPool threadPoolInstance) { // 通过原子操作将门控线程的运行状态设置为指定的运行次数。 int numRunsMask = Interlocked.Exchange(ref threadPoolInstance._separated.gateThreadRunningState, GetRunningStateForNumRuns(MaxRuns)); // 如果门控线程之前没有运行,就通知门控线程开始运行;如果门控线程不存在,就创建一个新的门控线程。 if (numRunsMask == GetRunningStateForNumRuns(0)) { RunGateThreadEvent.Set(); } else if ((numRunsMask & GateThreadRunningMask) == 0) { // 创建门控线程,使用new Thread()创建一个新的线程对象,然后设置其属性,包括将其标记为线程池线程、后台线程,并设置线程的名称。 CreateGateThread(); } }
我们可以从上面的源码中可以发现,ThreadPool对于线程的创建底层还是采用Thread对象进行维护,ThreadPool核心是在保持线程池的线程平衡和高效利用。
前面我们已经通过.NET的ThreadPool源码介绍了.NET线程池的实现原理和机制,接下来我看看线程池在实际的应用场景和使用建议。
1、关于 .NET 线程池使用场景有如下:
(1)、处理 I/O 密集型任务:当应用程序需要处理大量的 I/O 操作,例如文件读写、网络通信等,使用线程池可以避免频繁地创建和销毁线程,提高系统的性能和响应速度。
(2)、并发请求处理:当应用程序需要同时处理多个请求时,使用线程池可以减少线程的创建和销毁开销,提高系统的并发处理能力。
(3)、并行计算:当应用程序需要进行大量的计算操作时,使用线程池可以将计算任务分配给多个线程并发执行,提高计算效率和系统的吞吐量。
(4)、定时任务调度:当应用程序需要定期执行一些任务或者定时触发一些事件时,可以使用线程池配合定时器来实现定时任务调度。
(5)、长时间运行的任务:当应用程序需要执行一些耗时较长的任务,例如数据处理、图像处理等,可以使用线程池来异步执行这些任务,避免阻塞主线程。
2、关于 .NET 线程池使用建议如下:
(1)、适当配置线程池大小:根据应用程序的性能要求和系统资源的限制,合理配置线程池的大小。不要设置过大的线程池,以免消耗过多的系统资源,也不要设置过小的线程池,以免影响系统的并发处理能力。
(2)、合理选择线程池类型:根据任务的特性和执行要求,选择合适的线程池类型。例如,可以使用固定大小的线程池来处理固定数量的任务,使用缓存型线程池来处理大量的短期任务,使用定时线程池来执行定时任务调度等。
(3)、避免线程阻塞和死锁:在使用线程池时,要避免线程阻塞和死锁的问题。尽量避免在任务中进行长时间的阻塞操作,及时释放锁资源,防止出现死锁情况。
(4)、合理管理线程资源:要合理管理线程资源,及时回收和释放不再使用的线程资源,防止线程泄漏和资源浪费。
(5)、监控线程池运行情况:定期监控线程池的运行情况,包括线程池的大小、活动线程数、任务队列长度等指标,及时发现和解决潜在的问题和性能瓶颈。
本文截止到此已经结束,如本文有错漏之处还望大家多多指教。
浙公网安备 33010602011771号