天天看点

工作者线程(worker thread)和I/O线程

工作者线程(worker thread)和I/O线程

.NET中的术语工作者线程指的是任何线程而不是仅仅主线程。“工作者”的意思表示任何内容,包括等待IO端口完成,线程池会预先缓存一些工作者线程因为创建线程的代价比较昂贵。

.NET中的术语I/O线程指的是线程池中预先保留出来的部分线程,这部分线程的作用是为了分发从IOCP中的回调。CLR维护了自己的IOCP,它可以通过 ThreadPool.BindHandle方法绑定到任何一个操作系统句柄上。

.NET中的一些API方法,通过APM(异步编程模式),内部实现了ThreadPool.BindHandle方法。BeginXXX方法将用户的回调委托送到某个设备驱动程序,然后返回线程池。当做完成后,OS会通过IOCP提醒CLR它工作已经完成,当接收到通知后,I/O线程会醒来并且运行用户的回调。所以工作线程由开发人员调用,I/O线程由CLR调用。所以通常情况下,开发者并不会直接用到它。.

因此可以认为,工作者线程和I/O线程没有区别,它们都是普通的线程,但是CLR线程池中区分它们的目的是为了避免线程都去处理I/O回调而被耗尽,从而引发死锁。(设想,所有的工作者线程每一个都去等待I/O异步完成。)

开发人员需要关注的是确保I/O线程返回到线程池,I/O回调代码应该做尽量小的工作,并尽快返回到线程池。如果回调代码中的工作很多的话,应该考虑把工作拆分到一个工作者线程中去。否则,应用程序的风险是CLR线程池中保留I/O线程去做了工作者线程的活,可能导致死锁。

I/O线程以及IOCP

当执行I/O操作的时候,无论是同步I/O操作还是异步I/O操作,都会调用的Windows的API方法,比如,当读取文件的时候,调用ReadFile函数。该方法会将你的当前线程从用户态转变成内核态,会生成一个I/O请求包,并且初始化这个请求包,这个包中包含一个文件句柄,一个偏移量和一个Byte[]数组。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线程池队列。

之前说到过,在CLR内部维护了一个IOCP(I/O completion port),它提供了处理多个异步I/O请求的线程模型,可以把这个IOCP看做是一个消息队列,当一个进程创建了一个IOCP,即创建了一个队列。当异步I/O请 求完成时,设备驱动程序就会生成一个I/O完成包,将它按照FIFO方式排队列入该完成端口。之后,会由I/O线程提取完成I/O请求包,并调用之前的委托。注意:异步调用服务时,回调函数都是运行于CLR线程池的I/O线程当中。

在《Pro .NET Performance》中有如下一个示意图:

工作者线程(worker thread)和I/O线程

IOCP中有2个队列,一个是先进先出的队列,存放的是IO完成包,即已经完成的IO操作需要执行回调方法,因此先进先出的方式是非常公平的。

还有一个队列是线程队列,IOCP会预分配一些线程在这个队列中,这样会比即时创建线程处理I/O请求速度更快。这个队列是后进先出的,好处是下一个请求的到来可能还是用之前的线程来处理,就不需要进行线程上下文切换,提高了性能。

---------------------------------------------

参考资料

《CLR VIA C#》

《Pro .NET Performance》

《Windows核心编程(第5版)中文版》