本篇是異步編程系列的第三篇,原本計劃第三篇的內容是介紹異步編程中經常使用的幾個方法,可是前兩篇寫出來後,身邊的朋友老是會有其餘問題,因此決定再續寫一篇,做爲異步編程(一)和異步編程(二)的補充。html
本篇內容主要討論,在咱們的異步代碼裏,運行的究竟是哪一個線程,在執行長時間運行操做時線程發生了什麼。編程
在一個被async修飾了的異步方法裏,若是沒有遇到await,你的代碼將一直在調用線程上。在UI應用程序裏,好比ASP.NET或者WinForm程序裏,你的代碼會在ASP.NET工做線程或WinForm工做線程上運行。安全
咱們來看一下如下範例網絡
1: public async Task GetResultAsync()
2: {
3: Console.WriteLine();
4:
5: User user = this.GetUserAsync();
6:
7: //call other code
8:
9: return Task.CompletedTask;
10: }
以上範例裏,咱們在一個異步方法裏調用了另外一個異步方法,可是咱們並無使用await,這段代碼依然在原始調用線程上執行,此時這個方法只是扮演了一個傳播異步的做用。框架
當咱們在UI線程上如此編程的時候,代碼在UI線程是執行,在沒有執行結束以前,頁面是沒有響應的。因此若是頁面長時間沒有響應,未必是異步致使的,可能會有其餘緣由,須要綜合考慮,能夠藉助性能分析器來查看影響系統的緣由在哪裏。異步
代碼到達await後,究竟是哪個線程在執行異步操做呢。async
咱們以ASP.NET爲例,對於網絡請求之類的操做,此時沒有線程在執行異步操做,他們都被阻塞了,正在等待操做完成。可是若是使用了Task.Run,那麼執行該任務時就要用到線程池裏的線程了。異步編程
那麼問題來了,咱們在編寫異步方法的時候,確確實實能夠看到這個方法被執行了,確定有線程執行才行啊。性能
對的,確實須要線程來執行,這個線程咱們把它稱之爲是IO完成端口線程。此線程等待網絡請求完成,同時它在全部網絡請求之間共享。當網絡請求完成時,操做系統中的中斷處理程序會以Job方式添加到IO完成端口的隊列中。在請求發起後,響應返回前,它們須要依次由單個IO完成端口處理。優化
實際上,通常狀況下只有少許IO完成端口線程,以充分利用多個CPU核心。須要注意的是,不管當前有多少個請求,咱們的線程數量都是固定的。
參考如下運行圖
我在異步編程(一)這邊文章裏,有講到SynchronizationContext這個類,它是.NET框架提供的類,能夠在特定類型的線程中運行代碼。
.NET使用各類SynchronizationContext,常見的有ASP.NET、WinForms和WPF使用的UI線程上下文。SynchronizationContext的實例自己並無特殊的地方,其實例指向的是其子類,具備靜態成員,能夠用於讀取和控制當前的SynchronizationContext。
當前SynchronizationContext是當前線程的屬性。在一個特定線程所運行到的任意的地方,都可以獲取當前的SynchronizationContext並存儲它,而且可使用SynchronizationContext,在所啓動的這個特定線程上運行代碼。綜上所述,咱們並不須要知道代碼在哪一個線程上啓動,只須要使用到SynchronizationContext,咱們就能夠返回到啓動線程。
SynchronizationContext的重要方法是POST,它可使委託在正確的上下文中運行。
某些SynchronizationContext封裝單個線程,如UI線程。有些線程封裝了特定類型的線程,例如線程池,但能夠選擇將委託發送到其中的任何一個線程。有些不會更改代碼運行在哪一個線程上,而只用於監視,如ASP.NET SynchronizationContext。
到這個地方,咱們就須要瞭解一個問題了。在await以前,咱們的代碼是在調用線程上運行,那麼await以後,恢復方法時到了哪一個線程上了?
實際上,大多數狀況下,await後的代碼也由調用線程運行,儘管調用線程可能在等待期間作了其餘事情。C#使用SynchronizationContext來完成此操做。當等待任務完成時,當前的同步上下文被存儲爲暫停方法的一部分。而後,當方法恢復時,await關鍵字的基礎結構使用POST在捕獲的同步上下文上恢復該方法。
既然有大多數狀況,那麼確定也有小衆狀況吧,如下狀況能夠在不一樣的線程上運行
- SynchronizationContext具備多個線程,如線程池
- SynchronizationContext不是真正切換線程的上下文
- 到達等待時,沒有當前的同步上下文,例如在控制檯應用程序中。
- 將任務配置爲不使用同步上下文來恢復
注意:
對於UI應用程序來講,在同一線程上恢復是最重要的,咱們等待以後安全的操做UI。
以WinForm爲例,咱們設計一個按鈕,用於下載咱們喜歡的小圖標。用戶點擊按鈕以後,UI線程啓動,並會執行響應的操做,如下圖片展現了一個異步操做的流程,以及期間UI線程與IO線程是如何切換的
一、用戶單擊該按鈕,事件處理程序GetButton_OnClick開始排隊等待運行。
二、用戶界面線程執行GetButton_OnClick的前半部分,包括對GetFaviconAsync的調用。
三、UI線程繼續進入GetFaviconAsync並執行其前半部分,包括對DownloadDataTaskAsync的調用。
四、UI線程繼續進入DownloadDataTaskAsync,它啓動下載並返回任務。
五、UI線程離開DownloadDataTaskAsync,並返回GgetFaviconAsync處的await。
六、當前的UI線程捕獲到了SynchronizationContext。
七、GetFaviconAsyncy由於有await的標識,會等待,當DownloadDataTaskAsync完成後GetFaviconAsyncy便會使用捕獲到的SynchronizationContext恢復。
八、用戶線程離開GetFaviconAsync,並返回一個任務,並運行到GetButton_OnClick中的await。
九、相似地,GetButton_OnClick被等待暫停。
十、用戶線程離開GetButton_OnClick,可能會用於處理其餘操做。【此時,咱們正在等待圖標下載。可能須要幾秒鐘。注意,UI線程能夠自由處理其餘用戶操做,而IO完成端口線程還沒有涉及到。操做期間阻塞的線程總數爲零。】
十一、下載完成,所以IO完成端口在DownloadDataTaskAsync中對邏輯進行排隊處理。
十二、IO完成端口線程將把DownloadDataTaskAsync返回的任務設置爲完成。
1三、IO完成端口線程在任務內部運行代碼並處理完成,並會調用捕獲到的同步上下文(UI線程)上的POST以繼續運行接下來的代碼。
1四、IO完成端口線程被釋放並可能在其餘IO上工做。
1五、用戶界面線程找到POST指令,並繼續執行GetFaviconAsync的後半部分,直到結束。
1六、當UI線程離開GetFaviconAsync時,它會將GetFaviconAsync返回的任務設置爲完成。
1七、在這個運行點裏,當前的同步上下文與捕獲的上下文相同,於是無需用到POST,UI線程也會繼續同步進行。【此邏輯在WPF中是無效的,由於WPF常常建立新的SynchronizationContext對象。儘管它們是等效的,這使得TPL認爲它須要從新POST。】
1八、用戶線程繼續運行GetButton_OnClick的後半部分,直到結束。
同步上下文的每一個實現都是以不一樣的方式執行POST的,這是很是消耗性能的事情。爲了不這種開銷,.NET內部也是有本身的優化機制的,它會在捕獲的SynchronizationContext與任務完成時的當前上下文相同時,不使用POST。頗有意思的是,若是你使用調試器查看這種狀況,會發現調用堆棧是顛倒的。
可是,當同步上下文不一樣時,這就須要用到系統開銷了。在性能關鍵的代碼中或者某個代碼庫中,若是咱們並不不關心使用到了哪一個線程,這個時候咱們也能夠經過本身的手動操做來避開這種開銷。
在等待任務以前調用ConfigureaWait來完成。這樣就不會恢復到原始同步上下文。
1: byte[] bytes = await httpClient.PostAsJsonAsync(url,data).ConfigureAwait(false).ReadAsStreamAsync();
不過,ConfigureAwait並非嚴格的指令,它是.NET設計的一個標識,用來告訴運行時咱們不介意方法在哪一個線程上運行。若是該線程不重要(線程池線程),它將會繼續執行代碼。若是是很重要的線程,.NET會經過自身機制將線程釋放,讓它來作其餘事情,而方法也將在線程池中恢復。.NET使用線程的當前的SynchronizationContext來判斷它是否重要。
前文有說過,本文再提一次,在同步代碼中運行異步代碼,可能有隱藏的問題。Task有一個Result屬性,該屬性阻止等待任務完成。如如下代碼:
1: var result = GetUserAsync().Result;
可是若是在只有一個線程(如UI線程)的SynchronizationContext使用就會發生死鎖現象。解決問題的方法就是,咱們可使用線程池線程來解決這個問題。如如下代碼:
1: var result = Task.Run(() =>GetUserAsync()).Result;