async 與 await異步編程活用基礎

【本文轉自: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的部分

1程序員就是喜歡折騰,明明一句話能搞定的 恰恰寫這麼多行。

 

咱們如今看到的就是,程序進入一個又一個方法後,輸出個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        } 
複製代碼

 

 


程序寫出,編譯器沒有錯誤,運行->

2

  一個異步方法調用後將返回到它以前,它必須是完整的,而且線程依舊是活着的。

  而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介紹了三種可能的返回類型:TaskTask<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        } 
複製代碼

 

預料之中啊:

4

經過比較,你們不難看出哪一個實用哪一個不實用。

對於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點擊以後,應該出現的效果是:

5

  看圖片的效果不錯。

  接着你關掉提示框,你會發現 ,這個窗口點什麼都沒用了。關閉的不行,我肯定我說的沒錯。

  想關掉 就去任務管理器裏面結束進程吧~~~

  這是一個很簡單的死鎖示例,我想說的是差很少的代碼,在不一樣的應用程序裏面會有不同的效果,這就是它靈活和複雜的地方

  這種死鎖的根本緣由是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。而後它們就相互等待對方,從而致使死鎖,鎖上就不聽使喚了~~~用個圖來形容一下這個場景

777

說重點了。

  爲何控制帶應用程序不會造成這種死鎖?

  它們具備線程池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會幫咱們自動標識出來:

8

出這個問題是我在事件前加了一個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條,方便查閱。

9

   

  咱們都忽略了一個問題,可能你們歷來都沒想過,

咱們對代碼操做,一直都是一種異步編程。而咱們的代碼都運行在一個操做系統線程!

來看些最簡單的應用,幫助你們能快速的熟悉,並使用,纔是我想要達到的目的,你能夠不熟練,能夠不會用,可是,你能夠去主動接近它,適應它,熟悉它,直到徹底活用。

異步編程是重要和有用的。

下面來作些基本功的普及。我先前提到UI線程,什麼是UI線程?

咱們都遇見過程序假死狀態,凍結,無響應。

微軟提供了UI框架,使得你可使用C#操做全部UI線程,雖然說是UI框架,我想你們都聽過,它們包括:WinForms,WPF,Silverlight。

UI線程是惟一的一個能夠控制一個特定窗口的線程,也是惟一的線程能檢測用戶的操做,並對它們作出響應。

  此次介紹就到這了。

相關文章
相關標籤/搜索