既上一篇的一些觀念,來寫一些程式來實際驗證一下C#不同類型的專案上async/await跑起來會怎麼運作,執行緒會怎麼樣調用。
TL;DR,執行緒調用方式
上一篇C#的Async & Await筆記有較多的概念
這一篇的程式碼在這csharp-lab
- 一遇到await,執行緒不會馬上跳回到caller端,執行緒還是會往呼叫的methodAsync執行,直到最底層回傳一個Task,才開始依序跳回caller端,但如果Task已經是完成的狀態,則會省去原await機制,用原執行緒繼續執行。
- 一般Task不是已經完成的狀態下,遇到await,會優先看「當前」執行緒上有沒有SynchronizationContext,有則await後續的code會是原執行緒main thread來執行;沒有則await後續的code由thread pool裡面的執行緒執行。
- 一般Task不是已經完成的狀態下,遇到await加上ConfigureAwait(false),則await後續的code會由thread pool裡面的執行緒執行。
執行環境
硬體
- CPU:4 * Intel i7-6600U 2.6GHz
- RAM: 16.0 GB
- 系統類型: x64
- 作業系統:Windows 10
測試的project類型與target framework
- Console App: .NET Framework 4.6.1
- WPF: .NET Framework 4.6.1
- .NET Framework Web API: .NET Framework 4.6.1
- .NET Core Web API: .NET Core 2.1.1
測試方式
- 寫了一個library project TestClassLibrary裡面放一個AsyncAwaitTestClass.cs,所有要跑的code都寫在裡面,可以給.NET Framework和.NET Core參照與使用
- 下面跑測試會個別跑,會先註解掉其他測試來讓執行測試的起始情況一致
- 測試有寫的四種project類型,測試上不一樣的地方主要是WPF的main thread有SynchronizationContext,其他測試上的差別只有起始thread像WPF、Console App是main thread,還是像web server是調用worker thread的差別
- 因為有無SynchronizationContext,基本上只會有兩種較不一樣的執行結果,下面只寫出WPF和.NET Core的結果,想執行看看都還是可以抓回去玩玩看
- 印出測試訊息用Debug.WriteLine寫在output讓不同的project類型都會在同一個地方印出來,會透過執行AsyncAwaitTestClass.PrintInfos印出執行的當下有多少worker threads、iocp threads、total threads,印出當下執行緒的SynchronizationContext、ManagedThreadId、IsThreadPoolThread。
| 1 | private void PrintInfos() | 
測試一:執行續一遇到await就返回呼叫端?
(1)程式碼
| 1 | private async Task RunTest1() | 
(1)執行結果
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
不是一看到await,當前thread就跳回到caller,await裡面的code還是會先由當前thread執行,會直到執行到底層傳回task才「通常」開始一層一層返回caller。
可以在output上看到RunTest1的1.跟ReturnFinishedTaskAsync裡面的2.都是同一個thread ID,四種project結果都一樣。
其實想成下面這樣,程式是一樣的,應該就直覺async method裡面也會是同一個thread先執行。
| 1 | private async Task RunTest1() | 
測試二:await加不加ConfigureAwait(false)會發生什麼事,後續執行緒是?
(2.1)不加ConfigureAwait(false)
(2.1)程式碼
| 1 | private async Task RunTest2_1() | 
(2.1)執行結果
(2.1)WPF
執行httpClient.GetStringAsync前後的code會是一樣的thread
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.1).NET Core Web API
執行httpClient.GetStringAsync前後的code是不一樣的thread,後面是iocp thread
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.2)加ConfigureAwait(false)
(2.2)程式碼
| 1 | private async Task RunTest2_2() | 
(2.2)執行結果
(2.2)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.2).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.3)加ConfigureAwait(false)後面一定會是iocp thread?
(2.3)程式碼
| 1 | private async Task RunTest2_3() | 
(2.3)執行結果
如果跑的不是非同步IO當然不會是iocp thread
如果Task已經是完成的狀態,則會省去原await機制,用原執行緒繼續執行
(2.3)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.3).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.4)連續兩個await呼叫有加ConfigureAwait(false)
(2.4)程式碼
| 1 | private async Task RunTest2_4() | 
(2.4)執行結果
這邊訊息的2.跟3.有可能會是同一個iocp thread,畢竟我都對同一個URL發HTTP GET,有可能是cache導致Task很快就完成了,或是真的第二個await真的接手的thread跟第一個一樣
(2.4)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.4).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.5)先呼叫一個await有加ConfigureAwait(false),然後再呼叫一個await不加ConfigureAwait(false)?
(2.5)程式碼
| 1 | private async Task RunTest2_5() | 
(2.5)執行結果
WPF的第一個httpClient.GetStringAsync有加ConfigureAwait(false),所以await接手的會是iocp thread,可以看到2.的訊息顯示沒有SynchronizationContext,所以第二個httpClient.GetStringAsync即使沒有加ConfigureAwait(false),後續的code也不會是main thread
(2.5)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.5).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.6)外面的沒加ConfigureAwait(false),裡面的有加
(2.6)程式碼
| 1 | private async Task RunTest2_6() | 
(2.6)執行結果
在外層WPF因為有SynchronizationContext所以await後續還是main thread,.NET Core沒有SynchronizationContext所以裡面最後是iocp thread,外層也是iocp thread繼續 
(2.6)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.6).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.7)外面的有加ConfigureAwait(false),裡面的沒加
(2.7)程式碼
| 1 | private async Task RunTest2_7() | 
(2.7)執行結果
WPF因為裡面的沒加ConfigureAwait(false),所以await後續接手會是main thread,裡面最後執行完的是main thread,外層的有加ConfigureAwait(false),不交由main thread執行ConfigureAwait(false)的後續,所以由worker thread接手
(2.7)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(2.7).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
測試三:承測試二,如果不跑在IO相關的task,而跑在Task.Delay上,後續執行緒是?
(3.1)不加ConfigureAwait(false)
(3.1)程式碼
| 1 | private async Task RunTest3_1() | 
(3.1)執行結果
(3.1)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(3.1).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(3.2)加ConfigureAwait(false)
(3.2)程式碼
| 1 | private async Task RunTest3_2() | 
(3.2)執行結果
(3.2)WPF
| 1 | ===== Current Thread Info (in TestStart method) ===== | 
(3.2).NET Core Web API
| 1 | ===== Current Thread Info (in TestStart method) ===== |