這個問題在微信上被別人問過好多次,想來想去覺得有必要統一解答下,先說下我的答案:可能會,也有可能不會。 要想尋找答案,需要從 非同步處理 的底層框架說起。 一:非同步底層是什麼 非同步 從設計層面上來說它就是一個 發佈訂閱者 模式,畢竟它的底層用到了 埠完成隊列,可以從 IO完成埠內核對象 所提供的三 ...
這個問題在微信上被別人問過好多次,想來想去覺得有必要統一解答下,先說下我的答案:可能會,也有可能不會。
要想尋找答案,需要從 非同步處理
的底層框架說起。
一:非同步底層是什麼
非同步
從設計層面上來說它就是一個 發佈訂閱者
模式,畢竟它的底層用到了 埠完成隊列
,可以從 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 邏輯。