【本文轉自:http://www.cnblogs.com/x-xk/archive/2013/06/05/3118005.html 做者:肅】html
很久沒寫博客了,時隔5個月,奉上一篇精心準備的文章,但願你們能有所收穫,對async 和 await 的理解有更深一層的理解。程序員
async 和 await 有你不知道的祕密,微軟會告訴你嗎?編程
我用我本身的例子,去一步步詮釋這個技術,看下去,你絕對會有收穫。(漸進描述方式,願適應全部層次的程序員)框架
從零開始, 控制檯 Hello World:異步
什麼?開玩笑吧?拿異步作Hello World??async
下面這個例子,輸出什麼?猜猜?異步編程
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關鍵字。spa
重點在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介紹了三種可能的返回類型: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,那麼我在這說一句:
不管是什麼平臺(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線程是惟一的一個能夠控制一個特定窗口的線程,也是惟一的線程能檢測用戶的操做,並對它們作出響應。
此次介紹就到這了。