這個問題在微信上被別人問過好多次,想來想去覺得有必要統一解答下,先說下我的答案:可能會,也有可能不會,
要想尋找答案,需要從 異步處理 的底層框架說起,
一:異步底層是什么
異步 從設計層面上來說它就是一個 發布訂閱者 模式,畢竟它的底層用到了 埠完成佇列,可以從 IO完成埠內核物件 所提供的三個方法中有所體現,
- CreateIoCompletionPort
可以粗看下簽名:
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
這個方法主要是將 檔案句柄 和 IO完成埠內核物件 進行系結,其中的 NumberOfConcurrentThreads 表示完成埠最多允許 running 的執行緒上限,
- PostQueuedCompletionStatus
再看簽名:
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
這個函式的作用就是將一個 包 通過 內核物件 丟給 驅動設備程式 ,由后者與硬體互動,比如檔案,
- GetQueuedCompletionStatus
看簽名:
BOOL GetQueuedCompletionStatus(
[in] HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
[out] PULONG_PTR lpCompletionKey,
[out] LPOVERLAPPED *lpOverlapped,
[in] DWORD dwMilliseconds
);
這個方法嘗試從 IO完成埠內核物件 中提取 IO 包,如果沒有提取到,那么就會無限期等待,直到提取為止,
對上面三個方法有了概念之后,接下來看下結構圖:

這張圖非常言簡意賅,不過只畫了 埠完成佇列, 其實還有三個與IO執行緒有關的佇列,分別為:等待執行緒佇列, 已釋放佇列, 已暫停佇列,接下來我們稍微解讀一下,
當 執行緒t1 呼叫 GetQueuedCompletionStatus 時,假使此刻 任務佇列q1 無任務, 那么 t1 會卡住并自動進去 等待執行緒佇列 ,當某個時刻 q1 進了任務(由驅動程式投遞的),此時作業系統會將 t1 激活來提取 q1 的任務執行,同時將 t1 送到已釋放佇列中,
這個時候就有兩條路了,
- 遇到 Sleep 或者 lock 情況,
如果 t1 在執行的時候,遇到了 Sleep 或者 lock 鎖時需要被迫停止,此時系統會將 t1 執行緒送到 已暫停執行緒佇列 中,如果都 sleep 了,那 NumberOfConcurrentThreads 就會變為 0 ,此時就會遇到無人可用的情況,那怎么辦呢?只能讓系統從 執行緒池 中申請更多的執行緒來從 q1 佇列中提取任務,當某個時刻, 已暫停執行緒佇列 中的執行緒激活,那么它又回到了 已釋放佇列 中繼續執行任務,當任務執行完之后,再次呼叫 GetQueuedCompletionStatus 方法進去 等待執行緒佇列,
當然這里有一個問題,某一個時刻 等待執行緒佇列 中的執行緒數會暫時性的超過 NumberOfConcurrentThreads 值,不過問題也不大,
說了這么多理論是不是有點懵, 沒關系,接下來我結合 windbg 和 coreclr 原始碼一起看下,

以我的機器來說,IO完成埠內核物件 默認最多允許 12 個 running 執行緒,當遇到 sleep 時看看會不會突破 12 的限制,上代碼:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 2000; i++)
{
Task.Run(async () =>
{
await GetString();
});
}
Console.ReadLine();
}
public static int counter = 0;
static async Task<string> GetString()
{
var httpClient = new HttpClient();
var str = await httpClient.GetStringAsync("http://cnblogs.com");
Console.WriteLine($"counter={++counter}, 執行緒:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");
Thread.Sleep(1000000);
return str;
}
}

從圖中看,已經破掉了 12 的限制,那是不是 30 呢? 可以用 windbg 幫忙確認一下,
0:059> !tp
CPU utilization: 3%
Worker Thread: Total: 13 Running: 0 Idle: 13 MaxLimit: 2047 MinLimit: 12
Work Request in Queue: 0
--------------------------------------
Number of Timers: 1
--------------------------------------
Completion Port Thread:Total: 30 Free: 0 MaxFree: 24 CurrentLimit: 30 MaxLimit: 1000 MinLimit: 12
從最后一行看,沒毛病, IO完成埠執行緒 確實是 30 個,
在這種情況,異步操作一定會創建執行緒來處理
- 遇到耗時操作
所謂的耗時操作,大體上是大量的序列化,復雜計算等等,這里我就用 while(true) 模擬,因為所有執行緒都沒有遇到暫停事件,所以理論上不會突破 12 的限制,接下來稍微修改一下 GetString() 方法,
static async Task<string> GetString()
{
var httpClient = new HttpClient();
var str = await httpClient.GetStringAsync("http://cnblogs.com");
Console.WriteLine($"counter={++counter},時間:{DateTime.Now}, 執行緒:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");
while (true) { }
return str;
}

對比圖中的時間,過了30s也無法突破 12 的限制,畢竟這些執行緒都是 running 狀態并都在 已釋放佇列中,這也就造成了所謂的 請求無回應 的尷尬情況,
二:直面問題
如果明白了上面我所說的,那么 異步操作會不會創建執行緒 ? 問題,我的答案是 有可能會也有可能不會,具體還是取決于上面提到了兩種 callback 邏輯,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/454588.html
標籤:C#
