異步I/O不會創建新的線程
本文翻譯自 Stephen Cleary 的 [There is No Thread] 原文地址 https://blog.stephencleary.com/2013/11/there-is-no-thread.html
這是異步編程最基本的事實 : 異步I/O不會創建新的線程
反對這個事實的人很多,他們對此的看法是 "如果我await一個操作,那么一定會有一個線程正在等待,它可能是一個線程池的線程,或者是操作系統線程,或者是某個設備的驅動程序"
不用關注這些看法, 只需要記住如果異步操作是純粹的(方法全是async),那么就不會產生新的線程
不過這么一個簡單的結論顯然並不能夠讓他們信服,下面就看看異步到底發生了什么
讓我們跟蹤一個異步操作直到硬件層面,注:Net部分和設備驅動部分將被簡化描述 (因為細節太多了)
下面是一個通用的寫入操作 (例如 文件、網絡、USB面包機等等)
private async void Button_Click(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}
我們已經知道在await的時候UI線程不會被阻塞,那么問題來了: 是否是UI線程創建了另外一個線程殺了祭天換來了自己的自由....
關於UI線程是否犯下這一惡行,讓我們深入推斷一下
首先 : 看看類庫 (BCL的代碼) 假設 WriteAsync 是使用.Net中標准的異步方式平台調用(P/Invoke)I/O系統,這是一個基於異步方式的I/O,所以這會在設備的底層上啟動一個win32異步方式I/O操作句柄
然后操作系統會告訴設備驅動程序進行寫操作,代碼實現是構造出表示寫請求的對象,這個對象稱為I/O請求包(IRP)
設備驅動程序接收到IRP后會向設備發出一個命令來寫出數據,如果這個設備支持直接內存存取技術(DMA),只需將緩沖區地址寫入設備寄存器即可,設備驅動程序能做到將IRP標記為 "pending" 后交把控制權交還操作系統

在這里發現了真相的核心所在 : 在處理IRP時,設備驅動是不允許阻塞的。這意味着如果IRP不能立即完成,那么它必須異步處理,即使對於同步的api也是如此!在設備驅動級別,所有(有意義)的請求都是異步的
引用書籍里的知識 "無論是什么類型的I/O請求,應用程序向驅動程序發出的I/O操作都是異步執行的"
在IRP為 "pending" 時,操作系統返回到類庫,類庫將未完成的任務返回到應用程序的按鈕單擊事件方法,該方法掛起async方法,UI線程繼續執行
我們已經跟蹤請求到系統的盡頭 直到物理設備
現在寫入操作正在飛快的進行中,有多少個線程正在處理它?
答案是沒有
沒有設備驅動程序線程、操作系統線程、BCL線程或者寫在處理寫操作的線程池線程(🙂) 這里沒有創建任何新的線程
現在,讓我們跟蹤內核守護進程的響應回到正常的人類世界(被大神虐了)
寫入請求開始后的某個時間,設備完成了寫入操作。它會舉手報告中斷一下CPU
設備驅動程序的中斷服務程序 (ISR) 對中斷操作出響應,中斷是CPU級事件,它會臨時從正在運行的線程中奪取CPU的控制權。可以將ISR看作 "借用" 當前正在運行的線程,但我(原作者)更傾向於認為ISR非常低的級別上運行,以至於 "線程" 的概念還不存在 - 因此可以這么說,它們在線程的下方出現(在線程之前就有了ISR)
無論如何,ISR是正確的,因此它所做的就是告訴設備“謝謝您的中斷”,並對延遲過程調用(DPC)進行排隊
當CPU成功被中斷吸引注意力時,它會到DPCs(注: Distributed Process Control System 分布式處理控制系統),DPCs也是在一非常低的級別中運行(非常底層),說是它是"線程"並不完全正確,與ISR一樣,DPCs直接在CPU上運行,在線程系統的"下方"
DPC會把表示寫請求的IRP標記為 "complete" ,但是 "completion"狀態僅存在於操作系統級別,進程有自己的內存空間所以我們必須通知到它,因此操作系統將一個特殊內核態的異步過程調用(Asyncroneus Procedure Call 簡稱 APC) 排隊到擁有句柄的線程
由於類庫/BCL使用了標准的異步方式平台調用(P/Invoke)系統,所以它已經注冊了帶有I/O輸出完成端口(ICOP 注:I/Q Completion Port)的句柄,IOCP是線程池的一部分,因此簡單的借用I/O線程池線程來執行APC,APC通知Task已經完成
該Task已捕獲UI上下文,因此它不會直接在線程池線程上恢復異步方法,相反,它將該方法排隊到UI上下文中,UI線程會恢復執行該方法
因此,我們看到在請求在被處理時沒有創建新的線程,當請求完成后,各種線程被"借用",或者有一些處理被簡單的排隊等候,這個處理通常是在毫秒級時間內完成 (例如 在線程池中運行的APC),甚至到微秒級(例如 ISR),但是沒有線程被阻塞,只是等待該請求完成

我們在上面的樣例中使用了被簡化后最標准的方式,在實際開發中會比這個樣例復雜很多,但是核心的事實是一樣的
"必須有一個處理異步操作的線程" 並不是事實
打破常規,不要識圖去找到這個 "異步線程",這是不可能的。相反,要去尋找真相:
沒有線程
