async And await異步編程活用基礎


好久沒寫博客了,時隔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的部分

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線程是唯一的一個可以控制一個特定窗口的線程,也是唯一的線程能檢測用戶的操作,並對它們做出響應。

  這次介紹就到這了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM