好久沒寫博客了,時隔5個月,奉上一篇精心准備的文章,希望大家能有所收獲,對async 和 await 的理解有更深一層的理解。
async 和 await 有你不知道的秘密,微軟會告訴你嗎?
我用我自己的例子,去一步步詮釋這個技術,看下去,你絕對會有收獲。(漸進描述方式,願適應所有層次的程序員)
從零開始, 控制台 Hello World:
什么?開玩笑吧?拿異步做Hello World??
下面這個例子,輸出什么?猜猜?
1 static void Main(string[] args) 2 { 3 4 Task t = example1(); 5 } 6 7 static async Task DoWork() 8 { 9 10 Console.WriteLine("Hello World!"); 11 for (int i = 0; i < 3; i++) 12 { 13 Console.WriteLine("Working..{0}",i); 14 await Task.Delay(1000);//以前我們用Thread.Sleep(1000),這是它的替代方式。 15 } 16 } 17 static async Task example1() 18 { 19 await DoWork(); 20 Console.WriteLine("First async Run End"); 21 }
先不要看結果,來了解了解關鍵字吧,你確定你對async 和await了解?
async 其實就是一個標記,標記這個方法是異步方法。
當方法被標記為一個異步方法時,那么其方法中必須要使用await關鍵字。
重點在await,看字面意思是等待,等待方法執行完成。
它相當復雜,所以要細細講述:
當編譯器看到await關鍵字后,其后的方法都會被轉移到一個單獨的方法中去獨立運行。獨立運行?
是不是啟動了另一個線程?
嗯。有這個想法的同學,很不錯。就是這個答案。
我們來看看執行順序。來驗證一下我的這個說法,加深大家對await的理解。
首先從入口example1 進入:
——>碰見await Dowork()
——>此時主線程返回
——>進入DoWork
——>輸出“Hello World!”
——>碰見await 關鍵字
——>獨立執行線程返回
——>運行結束
我們看到3個輸出語句,按照我的說法,最終會出幾個?猜猜,動手驗證答案,往往是最實在的,大部分程序都不會騙我們。如果對Task有不熟悉的,可以參看本人博客先前寫Task的部分
我們現在看到的就是,程序進入一個又一個方法后,輸出個Hello World 就沒了,沒有結束,沒有跳出。因為是異步,所以我們看不到后續的程序運行了。
我為什么要用控制台來演示這個程序?
這個疑問,讓我做了下面的例子測試,一個深層次的問題,看不懂跳過沒有絲毫影響。
分析一下這個例子:
1 static void Main(string[] args) 2 { 3 example2(); 4 } 5 6 static async void example2() 7 { 8 await DoWork(); 9 Console.WriteLine("First async Run End"); 10 } 11 12 static async Task DoWork() 13 { 14 Console.WriteLine("Hello World!"); 15 for (int i = 0; i < 3; i++) 16 { 17 await Task.Delay(1000); 18 Console.WriteLine("Working..{0}",i); 19 } 20 }
運行絲毫問題,結果依舊是“Hello World ”,似乎更簡單了。
注意,細節來了,example2 是void,Mani也是void,這個相同點,似乎讓我們可以這么做:
1 static async void Main(string[] args)//給main加個 async 2 { 3 await DoWork(); 4 } 5 static async Task DoWork() 6 { 7 Console.WriteLine("Hello World!"); 8 for (int i = 0; i < 3; i++) 9 { 10 await Task.Delay(1000); 11 Console.WriteLine("Working..{0}",i); 12 } 13 }
程序寫出,編譯器沒有錯誤,運行->
一個異步方法調用后將返回到它之前,它必須是完整的,並且線程依舊是活着的。
而main正因為是控制台程序的入口,是主要的返回操作系統線程,所以編譯器會提示入口點不能用async。
下面這種事件,我想大家不會陌生吧?WPF似乎都用這種異步事件寫法:
1 private async void button1_Click(object sender, EventArgs e) 2 { 3 4 //…. 5 6 }
以此列Main入口,類推在ASP.NET 的 Page_Load上也不要加async,因為異步Load事件內的其他異步都會一起執行,死鎖? 還有比這更煩人的事嗎?winfrom WPF的Load事件目前沒有測試過,現在的事件都有異步async了,胡亂用,錯了你都不知道找誰。
好小細節提點到了,這個牽出的問題也就解決了。
有心急的同學可能就納悶了,第一個例子,怎么才能看到先前的輸出啊?
別急加上這句:
1 static void Main(string[] args) 2 { 3 Task t = example1(); 4 5 t.Wait();//add 6 }
輸出窗口就可以看到屏幕跳動連續輸出了、、、
入門示例已經介紹完了,來細細品味一下下面的知識吧。
Async使用基礎總結
到此Async介紹了三種可能的返回類型:Task,Task<T>和void。
但是async方法的固有返回類型只有Task和Task<T>,所以盡量避免使用async void。
並不是說它沒用,存在即有用,async void用於支持異步事件處理程序,什么意思?(比如我例子里面那些無聊的輸出呀..)或者就如上述提到的:
1 private async void button1_Click(object sender, EventArgs e) 2 3 { 4 5 //…. 6 7 }
有興趣的同學可以去找找(async void怎么支持異步事件處理程序)
異常處理介紹
async void 的方法具有不同的錯誤處理語義,因為在Task和Task<T>方法引發異常時,會捕獲異常並將其置於Task對象上,方便我們查看錯誤信息,而async void,沒有Task對象,沒有對象直接導致異常都會直接在SynchronizationContext上引發(SynchronizationContext是async 和 await的實現底層哦)既然提到了SynchronizationContext,那么我在這說一句:
SynchronizationContext 對任何編程人員來說都是有益的。
無論是什么平台(ASP.NET、Windows 窗體、Windows Presentation Foundation (WPF)、Silverlight 或其他),所有 .NET 程序都包含 SynchronizationContext 概念。(建議好學的同學去找找)
扯遠了,回到之前談到的 async void 和 Task 異常,看看兩種異常的結果,看看測試用例。
首先是 async void:
1 static void Main(string[] args) 2 { 3 AsyncVoidException(); 4 } 5 static async void ThrowExceptionAsync() 6 { 7 throw new OutOfMemoryException(); 8 } 9 static void AsyncVoidException() 10 { 11 try 12 { 13 ThrowExceptionAsync(); 14 } 15 catch (Exception) 16 { 17 18 throw; 19 } 20 }
沒有絲毫異常拋出,我先前說了,它會直接在SynchronizationContext拋出,但是執行異步的時候,它絲毫不管有沒有異常,執行線程直接返回,異常直接被吞,所以根本無法捕獲async void 的異常。我就不上圖了,偷懶了。。
再看看async Task測試用例:
1 static void Main(string[] args) 2 { 3 AsyncVoidException(); 4 } 5 static async Task ThrowExceptionAsync() 6 { 7 await Task.Delay(1000); 8 throw new OutOfMemoryException(); 9 } 10 static void AsyncVoidException() 11 { 12 try 13 { 14 Task t = ThrowExceptionAsync(); 15 t.Wait(); 16 } 17 catch (Exception) 18 { 19 20 throw; 21 } 22 }
預料之中啊:
通過比較,大家不難看出哪個實用哪個不實用。
對於async void 我還要閑扯一些缺點,讓大家認識到,用這個的確要有扎實的根底。
很顯然async void 這個方法未提供一種簡單的方式,去通知向調用它的代碼發出回饋信息,通知是否已經執行完成。
啟動async void方法不難,但你要確定它何時結束也是不易。
async void 方法會在啟動和結束時去通知SynchronizationContext。簡單的說,要測試async void 不是件簡單的事,但有心去了解,SynchronizationContext或許就不這么難了,它完全可以用來檢測async void 的異常。
說了這么多缺點,該突出些重點了:
建議多使用async Task 而不是async void。
async Task方法便於實現錯誤處理、可組合性和可測試性。
不過對於異步事件處理程序不行,這類處理程序必須返回void。
異步——我們既陌生又熟悉的朋友——死鎖!
對於異步編程不了解的程序員,或許常干這種事:
混合使用同步和異步代碼,他們僅僅轉換一小部分應用程序,提出一段代碼塊,然后用同步API包裝它,這么做方便隔離,同步分為一塊,異步分為另一塊,這么做的后果是,他們常常會遇到和死鎖有關的問題。
我之前一直用控制台來寫異步,大家應該覺得,異步Task就是這么用的吧?沒有絲毫阻噻,都是理所當然的按計划運行和結束。
嗯,來個簡單的例子,看看吧:
這是我的WPF項目的測試例子:
1 int i = 0; 2 private void button_1_Click(object sender, RoutedEventArgs e) 3 { 4 5 textBox.Text += "你點擊了按鈕 "+i++.ToString()+"\t\n"; 6 Task t = DelayAsync(); 7 t.Wait(); 8 } 9 private static async Task DelayAsync() 10 { 11 12 MessageBox.Show("異步完成"); 13 await Task.Delay(1000); 14 }
為了便於比較,看看控制台對應的代碼:
1 static void Main(string[] args) 2 { 3 Task t = DelayAsync(); 4 t.Wait(); 5 } 6 private static async Task DelayAsync() 7 { 8 await Task.Delay(1000); 9 Console.WriteLine("Complet"); 10 }
控制台程序沒有絲毫問題,我保證。
現在來注意一下WPF代碼,當我button點擊之后,應該出現的效果是:
看圖片的效果不錯。
接着你關掉提示框,你會發現 ,這個窗口點什么都沒用了。關閉的不行,我確定我說的沒錯。
想關掉 就去任務管理器里面結束進程吧~~~
這是一個很簡單的死鎖示例,我想說的是差不多的代碼,在不同的應用程序里面會有不一樣的效果,這就是它靈活和復雜的地方。
這種死鎖的根本原因是await處理上下文的方式。
默認情況下,等待未完成的Task時,會捕獲當前“上下文”,在Task完成時使用該上下文回復方法的執行(這里的“上下文”指的是當前TaskScheduler任務調度器)
值得注意的就是下面這幾句代碼:
1 t.Wait(); 2 3 private static async Task DelayAsync() 4 { 5 6 MessageBox.Show("異步完成"); 7 await Task.Delay(1000); 8 }
請確定你記住他的結構了,現在我來細講原理。
Task t 有一個線程塊在等待着 DelayAsync 的執行完成。
而 async Task DelayAsunc 在另一個線程塊中執行。
也就是說,在 MessageBox.Show("異步完成"); 這個方法完成后,await 會繼續獲取 async 余下的部分,它還能捕獲到接下來的代碼嗎?
async的線程已經被t線程在等待了,t在等待 async的完成,而運行Task.Delay(1000)后,await就會嘗試在捕獲的上下文中執行async方法的剩余部分,async被占用了,它就在等待t。然后它們就相互等待對方,從而導致死鎖,鎖上就不聽使喚了~~~用個圖來形容一下這個場景
說重點了。
為什么控制帶應用程序不會形成這種死鎖?
它們具有線程池SynchronizationContext(同步上下文),而不是每次執行一個線程塊區的SynchronizationContext,以此當await完成時,它會在線程池上安排async方法的剩余部分。所以各位,在控制台寫好的異步程序,移動到別的應用程序中就可能會發生死鎖。
好,現在來解決這個WPF的異步錯誤,我想這應該會引起大家興趣,解決問題是程序員最喜歡的活。
改Wait()為ConfigureAwait(false)像這樣:
1 Task t = DelayAsync(); 2 3 t.ConfigureAwait(continueOnCapturedContext:false);//這個寫法復雜了點,但從可讀性角度來說是不錯的,你這么寫t.ConfigureAwait(false)當然也沒問題
什么是ConfigureAwait?
官方解釋:試圖繼續回奪取的原始上下文,則為 true,否則為 false。
不好理解,我來詳細解釋下,這個方法是很有用的,它可以實現少量並行性:
使得某些異步代碼可以並行運行,而不是一個個去執行,進行零碎的線程塊工作,提高性能。
另一方面才是重點,它可以避免死鎖。
Wait造成的相互等待,在用這個方法的時候,就能順利完成,如意料之中自然。當然還有指導意見要說的,如果在方法中的某處使用ConfigureAwait,則建議對該方法中,此后每個await都使用它。
說到這,只怕有些同學覺得,能避免死鎖,這么好!以后就用ConfigureAwait就行了,不用什么await了。
沒有一種指導方式是讓程序員盲目使用的,ConfigureAwait這個方法,在需要上下文的代碼中是用不了的。看不懂?沒關系,接着看。
await運行的是一種原始上下文,就比如這樣:
1 static async Task example1() 2 { 3 await DoWork(); 4 Console.WriteLine("First async Run End"); 5 }
一個async對應一個await ,它們本身是一個整體,我們稱它為原始上下文。
ConfigureAwait而它有可能就不是原始上下文,因為它的作用是試圖奪回原始上下文。用的時候VS2012會幫我們自動標識出來:
出這個問題是我在事件前加了一個async聲明。
添加異步標識后,ConfigureAwait就不能奪取原始上下文了,在這種情況下,事件處理程序是不能放棄原始上下文。
大家要知道的是:
每個async方法都有自己的上下文,如果一個async方法去調用另一個async方法,則其上下文是相互獨立的。
為什么這么說?獨立是什么意思?我拿個例子說明吧:
1 private async void button_1_Click(object sender, RoutedEventArgs e) 2 { 3 Task t = DelayAsunc(); 4 5 t.ConfigureAwait(false);//Error 6 7 } 8 private static async Task DelayAsunc() 9 { 10 MessageBox.Show("異步完成"); 11 await Task.Delay(1000); 12 }
因為是獨立的,所以ConfigureAwait不能奪取原始上下文,錯誤就如上那個圖。
修改一下:
1 private async void button_1_Click(object sender, RoutedEventArgs e) 2 { 3 Task t = DelayAsunc(); 4 5 t.Wait(); 6 } 7 private static async Task DelayAsunc() 8 { 9 MessageBox.Show("異步完成"); 10 await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext:false); 11 }
每個async 都有自己的上下文,因為獨立所以它們之間的調用是可行的。
修改后的例子,將事件處理程序的所有核心邏輯都放在一個可測試且無上下文的async Task方法中,僅在上下文相關事件處理程序中保存最少量的代碼。
至此,已經總結了3條異步編程指導原則,我一起集合一下這3條,方便查閱。
我們都忽略了一個問題,可能大家從來都沒想過,
我們對代碼操作,一直都是一種異步編程。而我們的代碼都運行在一個操作系統線程!
來看些最簡單的應用,幫助大家能快速的熟悉,並使用,才是我想要達到的目的,你可以不熟練,可以不會用,但是,你可以去主動接近它,適應它,熟悉它,直到完全活用。
異步編程是重要和有用的。
下面來做些基本功的普及。我先前提到UI線程,什么是UI線程?
我們都碰見過程序假死狀態,凍結,無響應。
微軟提供了UI框架,使得你可以使用C#操作所有UI線程,雖說是UI框架,我想大家都聽過,它們包括:WinForms,WPF,Silverlight。
UI線程是唯一的一個可以控制一個特定窗口的線程,也是唯一的線程能檢測用戶的操作,並對它們做出響應。
這次介紹就到這了。