.net中ThreadPool与Task的认识总结
线程池和Task是多线程编程中两个经常使用的技术,大家在熟悉不过了。他们有什么关联关系?Task又是怎么工作的呢?估计很多时候会犯糊涂。通过翻阅资料,终于弄明白了,与大家分享一下。
工作线程与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的模式出队执行。