工作线程与I/O线程
在ThreadPool中有这样一个方法:
public static bool SetMaxThreads(int workerThreads, int completionPortThreads);
此方法中有两个参数:workerThreads和completionPortThreads。这两个参数引申出了两个概念:辅助线程(也叫工作线程)和异步 I/O 线程。这两个线程有什么区别么?通过查阅资料,我们可以了解到,工作线程其实就是我们编码主动向ThreadPool中创建的线程。而I/O线程是线程池中预先保留出来的部分线程,这部分线程的作用是为了分发从IOCP(I/O completion port) 中的回调。
那么什么是IOCP回调呢?
在CLR内部,系统维护了一个IOCP(I/O completion port),它提供了处理多个异步I/O请求的线程模型。我们可以把IOCP看做是一个消息队列。当一个进程创建了一个IOCP,即创建了一个队列。当异步I/O请 求完成时,设备驱动程序就会生成一个I/O完成包,将它按照FIFO方式排队列入该完成端口。之后,会由I/O线程提取完成I/O请求包,并调用之前的委托。注意:异步调用服务时,回调函数都是运行于CLR线程池的I/O线程当中。
I/O线程是由CLR调用的,通常情况下,我们不会直接用到它 。但是线程池中区分它们的目的是为了避免线程都去处理I/O回调而被耗尽,从而引发死锁。在编程时,开发人员需要关注的是确保I/O线程返回到线程池,I/O回调代码应该做尽量小的工作,并尽快返回到线程池,否则I/O线程会很快消耗光。如果回调代码中的工作很多的话,应该考虑把工作拆分到一个工作者线程中去。否则,I/O线程被耗尽,大量工作线程空闲,可能导致死锁。
再补充一下,当执行I/O操作的时候,无论是同步I/O操作还是异步I/O操作,都会调用的Windows的API方法,比如,当读取文件时,调用ReadFile函数。该方法会将你的当前线程从用户态转变成内核态,会生成一个I/O请求包,并初始化这个请求包。ReadFile会向内核传递,根据这个请求包,windows内核知道需要将这个I/O操作发送给哪个硬件设备。这些I/O操作会进入设备自己的处理队列中,该队列由这个设备的驱动程序维护。
如果此时是同步I/O操作,那么在硬件设备操作I/O的时候,发出I/O请求的线程由于无事可做被windows变成睡眠状态,当硬件设备完成操作后,再唤醒这个线程。这种方式非常直接,但是性能不高,如果请求数很多,那么休眠的线程数也很多,浪费了大量资源。
如果是异步I/O操作(.Net中,异步的I/O操作大部分为BeginXXX的形式 ),该方法在Windows把I/O请求包发送到设备的处理队列后就返回。同时,在调用异步I/O操作时,即调用BeginXXX方法的时候,需要传入一个委托,该委托方法会随着I/O请求包一路传递到设备的驱动程序。在设备处理完I/O请求包后,将该委托再放到CLR线程池队列。
总结来说,IOCP(I/O completion port)中有2个队列,一个是先进先出的队列,存放的是IO完成包,即已经完成的IO操作需要执行回调方法。还有一个队列是线程队列,IOCP会预分配一些线程在这个队列中,这样会比即时创建线程处理I/O请求速度更快。这个队列是后进先出的,好处是下一个请求的到来可能还是用之前的线程来处理,就不需要进行线程上下文切换,提高了性能。
这里有一个IOCP的解释,写的很好。http://gamebabyrocksun.blog.163.com/blog/static/57153463201036104134250/
Task的运行原理分析
Task与ThreadPool什么关系呢?简单来说,Task是基于ThreadPool实现的,当然被标记为LongRunning的Task(单独创建线程实现)除外。Task被创建后,通过TaskScheduler执行工作项的分配。TaskScheduler会把工作项存储到两类队列中: 全局队列与本地队列。全局队列被设计为FIFO的队列。本地队列存储在线程中,被设计为LIFO.
当主程序创建了一个Task后,由于创建这个Task的线程不是线程池中的线程,则TaskScheduler 会把该Task放入全局队列中。
如果这个Task是由线程池中的线程创建,并且未设置TaskCreationOptions.PreferFairness标记(默认情况下未设置),TaskScheduler 会把该Task放入到该线程的本地队列中。如果设置了TaskCreationOptions.PreferFairness标记,则放入全局队列。
官方的解释是: *任务(即不在其他任务的上下文中创建的任务)与任何其他工作项一样放在全局队列上。 但是,嵌套任务或子任务(在其他任务的上下文中创建)的处理方式大不相同。 子任务或嵌套任务放置在特定于执行父任务的线程的本地队列上。 父任务可能是*任务,也可能是其他任务的子任务。
那么任务放入到两类队列中后,是如何被执行的呢?
当线程池中的线程准备好执行更多工作时,首先查看本地队列。 如果工作项在此处等待,直接通过LIFO的模式获取执行。 如果没有,则向全局队列以FIFO的模式获取工作项。如果全局队列也没有工作项,则查看其他线程的本地队列是否有可执行工作项,如果存在可执行工作项,则以FIFO的模式出队执行。
There is one thread pool per process. Beginning with the .NET Framework 4, the default size of the thread pool for a process depends on several factors, such as the size of the virtual address space. A process can call the GetMaxThreads method to determine the number of threads. The number of threads in the thread pool can be changed by using the SetMaxThreads method. Each thread uses the default stack size and runs at the default priority.
Note
Unmanaged code that hosts the .NET Framework can change the size of the thread pool by using the CorSetMaxThreads
function, defined in the mscoree.h file.
The thread pool provides new worker threads or I/O completion threads on demand until it reaches the minimum for each category. When a minimum is reached, the thread pool can create additional threads in that category or wait until some tasks complete. Beginning with the .NET Framework 4, the thread pool creates and destroys worker threads in order to optimize throughput, which is defined as the number of tasks that complete per unit of time. Too few threads might not make optimal use of available resources, whereas too many threads could increase resource contention.
Note
When demand is low, the actual number of thread pool threads can fall below the minimum values.
You can use the GetMinThreads method to obtain these minimum values.
资料参考:
https://www.cnblogs.com/vveiliang/p/7943003.html
https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool?redirectedfrom=MSDN&view=netframework-4.7.2