async-await 線程分析


這里沒有線程

原文地址:https://blog.stephencleary.com/2013/11/there-is-no-thread.html

前言

我是在看 C#8.0 新特性異步流時在評論里看到這篇文章的,閱讀之后發現這篇文章干貨滿滿,作者解釋的非常清晰,里面的本質分析內容在《CLR via C#》一書中也有講到。更加加深了我的印象。遂在這里翻譯過來,以便加深自己的理解

正文

一個本質的事實就是純粹的異步是不會產生線程的

反對這個事實的人有很多。“不”,他們喊道:“如果我正在等待一個操作,那么這個線程就必須在執行等待!它可能是線程池線程。或者是一個操作系統(OS)線程,又或是其他設備驅動程序...”

無需理會他們。如果一個異步操作是純粹的(pure),那么就不會有線程的。

那些持懷疑態度的人。那我們就迎合他們罷。

我們將跟蹤一個異步操作一直到硬件,特別是 .net 部分和硬件部分。我們必須通過省略一些中間的細節來簡化描述,但是我們不能偏離事實真相。

通常寫一個異步操作(文件,網絡流,USB 接口等等)。代碼如下:

private async void Button_Click(object sender, RoutedEventArgs s)
{
    byte[] data = ...
    await myDevice.WriteAsync(data, 0, data.Length);
}

我們已經知道在 await 的時候 UI 線程是不會阻塞的。那么問題來了:這里有沒有是其他線程在阻塞期間犧牲自己以至於讓 UI 線程存活呢?

那讓我們繼續往下深究

第一步:庫(比如進入到 BCL 庫源代碼)。我們假使 WriteAsync 是通過 [http://msdn.microsoft.com/en-us/library/system.threading.overlapped.aspx](P/Invoke 在 .net 異步的標准實現) 來實現的,它是基於overllapped I/O 的。所有它在一個設備驅動程序的句柄上開始一個 Win32 overlapped 操作。

OVERLAPPED 是一個包含了用於異步輸入輸出的信息的結構體;詳細解釋移步 https://en.wikipedia.org/wiki/Overlapped_I/O

操作系統然后就會轉向設備驅動程序並開始請求一個寫操作。它首先會構造一個表示寫請求的對象;它被稱為 I/O Request Packet(IRP)。

設備驅動程序接受到 IRP 之后並向設備提交一個寫出數據的命令。如果這個設備支持直接內存訪問(DMA 全稱是 Direct Memory Access),這能夠像寫到緩沖區到寄存器一樣簡單。這就是設備驅動程序所做的一切;它把 IRP 標記為 "pending" (掛起) 並返回給操作系統。

本質核心就在這里:設備驅動程序正在處理 IRP 時是不會允許阻塞的。也就是說如果 IRP 不能馬上完成,那么它必須要異步處理。甚至是同步 API 也是如此!在設備驅動程序級別,所有的請求(重要的)都是異步的。

這里引用了 Tomes知識,“無論I/O請求的類型如何,在內部,代表應用程序向驅動程序發出的I/O操作都是異步執行的”

在 IRP 掛起的時候,OS 返回庫,庫返回了一個未完成的任務給按鈕點擊事件,並暫停了 async 方法, UI 線程繼續執行。

我們跟着請求繼續往下走,現在到達了設備的物理層

現在寫操作正在進行。那么有多少線程正處理它呢?

沒有。

這里沒有設備驅動程序線程、OS 線程、庫(BCL)線程或者是線程池線程操作寫操作。這里沒有線程

現在我們來跟着從來自內核的相應回到最初的世界。

在開始寫請求之后的一段時間,設備完成了寫操作。它會以中斷的方式來通知 CPU。

設備驅動程序的中斷服務程序(ISR(Interrupt Service Routine) )響應中斷。這個中斷是 CPU 級別的事件,無論哪個線程正在運行都會臨時的搶占 CPU 的控制權。你可以認為 ISR 是在“借”當前正在運行的線程,但是我更傾向於 ISR 運行時的級別非常低,以至於不存在“線程”的概念。可以這么說,它們在所有線程之下進來的。

不管怎樣,ISR 正確寫完了,完了它會通知設備程序 “謝謝你的中斷” 並且進入 DPC(Deffered Procedure Call) 隊列(延遲過程調用)

當 CPU 被中斷干擾時,它將會到達 DPCs。DPCs 也會執行在一個很低的級別以至於說它是一個線程是不正確的;就像 ISRs,DPCs 直接在 CPU 上運行,在線程系統之下。

PDC 接受代表寫請求的 IRP 並且標記為 “已完成”。然而,這個“完成”狀態只存在於 OS 級別;進程有它自己的內存空間,它必須被通知。所以 OS 會入隊列一個特殊內核模式異步過程調用(APC)到擁有自己句柄的線程。

由於 BCL 庫使用了標准的 P/Invoke overlapped I/O 系統,它已經在 I/O Completion Port(IOCP)注冊句柄,它是線程池的一部分。所以借用 I/O 線程池線程來執行 APC,它會通知這些任務已經完成了。

這個任務已經捕捉了 UI 上下文,所以它不會直接在線程池線程上恢復異步方法。而是它將該方法的延續排隊到 UI 上下文中,並且 UI 線程將恢復執行那個方法。

所以我們看到,正當一個請求處理時這里是沒有線程的。當請求完成時,一些線程被借過去或者是被短暫的排隊。這項工作通常在 1 毫秒左右(例如 APC 運行在線程池線程)或 1 微妙左右(例如 ISR)。但是這里沒有線程是阻塞的,僅僅只是等待請求完成。

現在,我們遵循的路徑是標准路徑,這是如此清晰簡單。這里有無數的變量,但是核心是不變的。

所以說 “這里必須有一個線程是在處理異步操作” 是不正確的。

釋懷吧,不要嘗試找到異步線程——這是不可能的。而是你應該去了解真相:

沒有線程

該篇文章的評論也是很精彩的,特別是討論將異步操作當作消息處理那部分討論,建議也花時間看下

本文同步至:https://github.com/MarsonShine/MarsonShine.github.io/blob/master/mardown/async/There-IS-NO-Thread.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM