工作者線程(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》中有如下一個示意圖:

IOCP中有2個隊列,一個是先進先出的隊列,存放的是IO完成包,即已經完成的IO操作需要執行回調方法,是以先進先出的方式是非常公平的。
還有一個隊列是線程隊列,IOCP會預配置設定一些線程在這個隊列中,這樣會比即時建立線程處理I/O請求速度更快。這個隊列是後進先出的,好處是下一個請求的到來可能還是用之前的線程來處理,就不需要進行線程上下文切換,提高了性能。
---------------------------------------------
參考資料
《CLR VIA C#》
《Pro .NET Performance》
《Windows核心程式設計(第5版)中文版》