說來真的好久沒有寫文章了,除了工作很忙之外,目前碰到的問題也通常不是一兩篇文章可以交代的清,所以也就更新部落格的時間也就間隔越來越長了。
這次主要是想把最近針對 Async 的一些測試研究記錄下來,那我們就開始吧 !
# Async 是什麼?
Async 在 C# 語言中用來支援非同步處理的一種語法,而它的使用往往搭配 Await 一起使用,先來看看一段簡單的程式碼
1 | class Program |
這段程式碼執行後可以看到以下結果
1 | 1. Hi I'm Async Demo |
觀察執行緒的切換
接著加上執行緒 Id 看看,讓我們更清楚執行緒之間是如何切換的
執行結果
1 | [1] 1. Hi I'm Async Demo |
執行流程
執行緒_1 在 11 行印出了
[1] 1. Hi I'm Async Demo
執行緒_1 在 14 行進入了 DoAsync 的方法中
執行緒_1 在 27 行碰到 Task.Run 開啟了非同步執行的方法,並因為 await 跳出了這個 DoAsync()
執行緒_1 在 16 行碰到印出了
[1] 2. Hello World!
執行緒_1 在 19 行碰到 await task ,開始等待 task 執行完成,執行緒_1 釋放回到 Thread Pool
執行緒_4 在非同步方法的第 30 行 印出了
[4] 3. Async method Done
,並通知 await task 執行完成TaskScheduler(通常,這會是以執行緒集區為目標的預設工作排程器)依據最有效率的判斷,讓 執行緒_4 往下執行未完的部分
執行緒_4 接手繼續將 await 之後還沒做完的工作執行完成,意即 21 行,印出
[4] 4. End!
稍微修改一下程式…
1 | class Program |
執行結果
1 | [1] 1. Hi I'm Async Demo |
這邊可以看到 執行緒_1 在執行到 await Task.Run(()=> …) 時跳出,但因為我們下了 Wait(),所以強迫 執行緒_1 進行同步的等待。
執行緒_4 印完 [4] 3. Async method Done
後,通知 執行緒_1 繼續往下進行,所以看到 執行緒_1 接著將剩下的程式跑完
差別在哪?
當呼叫非同步執行方法時,如果一路都是用 await 並不會造成任何執行緒被封鎖,換言之該執行緒還可以在別的地方繼續服務,一旦呼叫了 Result 或 Wait() 這類的強制同步方法,則該執行緒會被封鎖,並等待到非同步方法執行完成後才繼續完成尚未完成的後續工作,這會嚴重消耗執行緒的使用效率。
我曾經對一個 Web API 專案進行壓測,在資源給得非常有限的情境下 (約 0.5 core cpu),await 搭配 Task.Run()
跟 Result 搭配 Task.Run
,兩個 RPS 測起來差了快一倍之多,在資源極度有限下,執行緒的使用效率將大大影響整體服務的效率。
# SynchronizationContext
SynchronizationContext 是用來記錄當前執行緒環境的類別,在 ASP.NET、WPF、WinForm 都有類似的類別只是名字可能有些差異,其最主要的目的都是在非同步方法中要能調用 UI 執行緒來更新介面之類的操作,而 SynchronizationContext 就紀錄著 UI 執行緒。
Deadlock
過去寫 ASP.NET 的時候曾經踩過一次 SynchronizationContext 的雷,在 ASP.NET 中如果如果呼叫 SynchronizationContext.Currnt
會發現並不為 Null,且型別是 AspNetSynchronizationContext
,當我們用上述的 Result / Wait() 搭配 await
將會導致 Deadlock。
原因是程式碰到 await 時會先判斷 SynchronizationContext.Currnt 是否為 Null,如果是則會在 Task 結束時呼叫 TaskScheduler (通常為 Thread Pool)來安排後續工作。反之,如果 SynchronizationContext.Currnt 不為 Null 時,就會透過 SynchronizationContext.Currnt 來繼續後續的動作。
1 | //碰到 await 時,記住了 SynchronizationContext.Currnt |
而 .Result / Wait() 的方法會封鎖住 SynchronizationContext.Currnt,造成兩邊互等的情況發生,產生了 Deadlock
1 | DoAsync().Wait(); //封鎖 SynchronizationContext.Currnt 的物件等待 Task 完成 |
而這只會發生在 SynchronizationContext.Currnt 不為 Null 的系統中,像是前述提到的 ASP.NET、WPF、WinForm,在 Console Application 並不會發生。
解決方法 ConfigureAwait(false)
如果要避免上述所提到的 Deadlock ,可以在呼叫非同步方法時加上 ConfigureAwait(false),這樣非同工作完成時就會透過另一條執行緒繼續完成後續工作
1 | await Task.Run(()=> |
在 ASP.Net Core 的時代…
在 ASP.Net core 呼叫 SynchronizationContext.Current 現在只會得到 Null ,換言之剛剛發生 deadlock 的情境已經不會發生,所以過往在非同步的地方常常要用 ConfigureAwait(false) 可以不用寫了,不過如果你是寫元件或 SDK 類的,並無法預測會被使用在怎樣的環境的話,建議還是都加上會比較保險。
# Best Practice
在使用 Async / await 等非同步技巧時,最好的方式還是都盡量使用非阻斷式的寫法 await ,避免使用 Result / Wait() ,即使你已經是寫 ASP.Net core 不會發生 Deadlock 的情況,阻斷式的寫法對於執行緒的使用效率來說還是會有影響的。