既上一篇的一些觀念,來寫一些程式來實際驗證一下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) ===== |