進階系列(12)—— C#異步編程

1、What's 異步?

     啓動程序時,系統會在內存中建立一個新的進程。進程是構成運行程序資源的集合。數據庫

     在進程內部,有稱爲線程的內核對象,它表明的是真正的執行程序。系統會在 Main 方法的第一行語句就開始線程的執行。編程

     線程:服務器

     ①默認狀況,一個進程只包含一個線程,從程序的開始到執行結束;網絡

     ②線程能夠派生自其它線程,因此一個進程能夠包含不一樣狀態的多個線程,來執行程序的不一樣部分;dom

     ③一個進程中的多個線程,將共享該進程的資源;異步

     ④系統爲處理器執行所規劃的單元是線程,而非進程。async

     通常來講咱們寫的控制檯程序都只使用了一個線程,從第一條語句按順序執行到最後一條。但在不少的狀況下,這種簡單的模型會在性能或用戶體驗上很差。ide

     例如:服務器要同時處理來自多個客戶端程序的請求,又要等待數據庫和其它設備的響應,這將嚴重影響性能。程序不該該將時間浪費在響應上,而要在等待的同時執行其它任務!異步編程

     如今咱們開始進入異步編程。在異步程序中,代碼不須要按照編寫時的順序執行。這時咱們須要用到 C# 5.0 引入的 async/await 來構建異步方法。性能

     咱們先看一下不用異步的示例:

 

 1     class Program  2  {  3 //建立計時器  4 private static readonly Stopwatch Watch = new Stopwatch();  5  6 private static void Main(string[] args)  7  {  8 //啓動計時器  9  Watch.Start(); 10 11 const string url1 = "http://www.cnblogs.com/"; 12 const string url2 = "http://www.cnblogs.com/liqingwen/"; 13 14 //兩次調用 CountCharacters 方法(下載某網站內容,並統計字符的個數) 15 var result1 = CountCharacters(1, url1); 16 var result2 = CountCharacters(2, url2); 17 18 //三次調用 ExtraOperation 方法(主要是經過拼接字符串達到耗時操做) 19 for (var i = 0; i < 3; i++) 20  { 21 ExtraOperation(i + 1); 22  } 23 24 //控制檯輸出 25 Console.WriteLine($"{url1} 的字符個數:{result1}"); 26 Console.WriteLine($"{url2} 的字符個數:{result2}"); 27 28  Console.Read(); 29  } 30 31 /// <summary> 32 /// 統計字符個數 33 /// </summary> 34 /// <param name="id"></param> 35 /// <param name="address"></param> 36 /// <returns></returns> 37 private static int CountCharacters(int id, string address) 38  { 39 var wc = new WebClient(); 40 Console.WriteLine($"開始調用 id = {id}:{Watch.ElapsedMilliseconds} ms"); 41 42 var result = wc.DownloadString(address); 43 Console.WriteLine($"調用完成 id = {id}:{Watch.ElapsedMilliseconds} ms"); 44 45 return result.Length; 46  } 47 48 /// <summary> 49 /// 額外操做 50 /// </summary> 51 /// <param name="id"></param> 52 private static void ExtraOperation(int id) 53  { 54 //這裏是經過拼接字符串進行一些相對耗時的操做 55 var s = ""; 56 57 for (var i = 0; i < 6000; i++) 58  { 59 s += i; 60  } 61 62 Console.WriteLine($"id = {id} 的 ExtraOperation 方法完成:{Watch.ElapsedMilliseconds} ms"); 63  } 64 }

 

   

 

 

 

 

 圖1-1 運行的效果圖,以毫秒(ms)爲單位

【備註】通常來講,直接拼接字符串是一種比較耗性能的手段,若是對字符串拼接有性能要求的話應該使用 StringBuilder。

【注意】每次運行的結果可能不一樣。無論哪次調試,絕大部分時間都浪費前兩次調用(CountCharacters 方法),即在等待網站的響應上。

 

  

 

 

 

圖1-2 根據執行結果所畫的時間軸

 

有人曾幻想着這樣提升性能的方法:在調用 A 方法時,不等它執行完,直接執行 B 方法,而後等 A 方法執行完成再處理。

 C# 的 async/await 就能夠容許咱們這麼弄。

 1     class Program  2  {  3 //建立計時器  4 private static readonly Stopwatch Watch = new Stopwatch();  5  6 private static void Main(string[] args)  7  {  8 //啓動計時器  9  Watch.Start(); 10 11 const string url1 = "http://www.cnblogs.com/"; 12 const string url2 = "http://www.cnblogs.com/liqingwen/"; 13 14 //兩次調用 CountCharactersAsync 方法(異步下載某網站內容,並統計字符的個數) 15 Task<int> t1 = CountCharactersAsync(1, url1); 16 Task<int> t2 = CountCharactersAsync(2, url2); 17 18 //三次調用 ExtraOperation 方法(主要是經過拼接字符串達到耗時操做) 19 for (var i = 0; i < 3; i++) 20  { 21 ExtraOperation(i + 1); 22  } 23 24 //控制檯輸出 25 Console.WriteLine($"{url1} 的字符個數:{t1.Result}"); 26 Console.WriteLine($"{url2} 的字符個數:{t2.Result}"); 27 28  Console.Read(); 29  } 30 31 /// <summary> 32 /// 統計字符個數 33 /// </summary> 34 /// <param name="id"></param> 35 /// <param name="address"></param> 36 /// <returns></returns> 37 private static async Task<int> CountCharactersAsync(int id, string address) 38  { 39 var wc = new WebClient(); 40 Console.WriteLine($"開始調用 id = {id}:{Watch.ElapsedMilliseconds} ms"); 41 42 var result = await wc.DownloadStringTaskAsync(address); 43 Console.WriteLine($"調用完成 id = {id}:{Watch.ElapsedMilliseconds} ms"); 44 45 return result.Length; 46  } 47 48 /// <summary> 49 /// 額外操做 50 /// </summary> 51 /// <param name="id"></param> 52 private static void ExtraOperation(int id) 53  { 54 //這裏是經過拼接字符串進行一些相對耗時的操做 55 var s = ""; 56 57 for (var i = 0; i < 6000; i++) 58  { 59 s += i; 60  } 61 62 Console.WriteLine($"id = {id} 的 ExtraOperation 方法完成:{Watch.ElapsedMilliseconds} ms"); 63  } 64 }

 

 

 

 

 

 

 圖1-3 修改後的執行結果圖

 

 

 

圖1-4 根據加入異步後的執行結果畫的時間軸。

 

咱們觀察時間軸發現,新版代碼比舊版快了很多(因爲網絡波動的緣由,極可能會出現耗時比以前長的狀況)。這是因爲 ExtraOperation 方法的數次調用是在 CountCharactersAsync 方法調用時等待響應的過程當中進行的。全部的工做都是在主線程中完成的,沒有建立新的線程。

【改動分析】只改了幾個細節的地方,直接展開代碼的話可能看不出來,改動以下:

   

 圖1-5

  圖1-6

 

  

 

①從 Main 方法執行到 CountCharactersAsync(1, url1) 方法時,該方法會當即返回,而後纔會調用它內部的方法開始下載內容。該方法返回的是一個 Task<int> 類型的佔位符對象,表示計劃進行的工做。這個佔位符最終會返回 int 類型的值。

②這樣就能夠沒必要等 CountCharactersAsync(1, url1) 方法執行完成就能夠繼續進行下一步操做。到執行 CountCharactersAsync(2, url2)  方法時,跟 ① 同樣返回 Task<int> 對象。

③而後,Main 方法繼續執行三次 ExtraOperation 方法,同時兩次 CountCharactersAsync 方法依然在持續工做 。

④t1.Result 和 t2.Result 是指從 CountCharactersAsync 方法調用的 Task<int> 對象取結果,若是尚未結果的話,將阻塞,直有結果返回爲止。

 

2、async/await 結構

     先解析一下專業名詞:

     同步方法:一個程序調用某個方法,等到其執行完成以後才進行下一步操做。這也是默認的形式。

     異步方法:一個程序調用某個方法,在處理完成以前就返回該方法。經過 async/await 咱們就能夠實現這種類型的方法。

     async/await 結構可分紅三部分:

     (1)調用方法:該方法調用異步方法,而後在異步方法執行其任務的時候繼續執行;

     (2)異步方法:該方法異步執行工做,而後馬上返回到調用方法;

     (3)await 表達式:用於異步方法內部,指出須要異步執行的任務。一個異步方法能夠包含多個 await 表達式(不存在 await 表達式的話 IDE 會發出警告)。

  如今咱們來分析一下示例。

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

                                                圖2-1

 3、What’s 異步方法

     異步方法:在執行完成前當即返回調用方法,在調用方法繼續執行的過程當中完成任務。
     語法分析:
     (1)關鍵字:方法頭使用 async 修飾。
     (2)要求:包含 N(N>0) 個 await 表達式(不存在 await 表達式的話 IDE 會發出警告),表示須要異步執行的任務。
     (3)返回類型:只能返回 3 種類型(void、Task 和 Task<T>)。Task 和 Task<T> 標識返回的對象會在未來完成工做,表示調用方法和異步方法能夠繼續執行。
     (4)參數:數量不限,但不能使用 out 和 ref 關鍵字。
     (5)命名約定:方法後綴名應以 Async 結尾。
     (6)其它:匿名方法和 Lambda 表達式也能夠做爲異步對象;async 是一個上下文關鍵字;關鍵字 async 必須在返回類型前。
 

圖3-1 異步方法的簡單結構圖

(一)介紹異步方法

     異步方法:在執行完成前當即返回調用方法,在調用方法繼續執行的過程當中完成任務。
     語法分析:
     (1)關鍵字:方法頭使用 async 修飾。
     (2)要求:包含 N(N>0) 個 await 表達式(不存在 await 表達式的話 IDE 會發出警告),表示須要異步執行的任務。【備註】感謝 czcz1024 的修正與補充:沒有的話,就和普通方法同樣執行了。
     (3)返回類型:只能返回 3 種類型(void、Task 和 Task<T>)。Task 和 Task<T> 標識返回的對象會在未來完成工做,表示調用方法和異步方法能夠繼續執行。
     (4)參數:數量不限。但不能使用 out 和 ref 關鍵字。
     (5)命名約定:方法後綴名應以 Async 結尾。
     (6)其它:匿名方法和 Lambda 表達式也能夠做爲異步對象;async 是一個上下文關鍵字;關鍵字 async 必須在返回類型前。
 
 
 
 
 
 
 
 
 
                          圖1 異步方法的簡單結構圖

  關於 async 關鍵字:

  ①在返回類型以前包含 async 關鍵字

  ②它只是標識該方法包含一個或多個 await 表達式,即,它自己不建立異步操做。

  ③它是上下文關鍵字,便可做爲變量名。

  如今先來簡單分析一下這三種返回值類型:void、Task 和 Task<T>

  (1)Task<T>:調用方法要從調用中獲取一個 T 類型的值,異步方法的返回類型就必須是Task<T>。調用方法從 Task 的 Result 屬性獲取的就是 T 類型的值。

 1         private static void Main(string[] args)  2  {  3 Task<int> t = Calculator.AddAsync(1, 2);  4  5 //一直在幹活  6  7 Console.WriteLine($"result: {t.Result}");  8  9  Console.Read(); 10 }
 1     internal class Calculator  2  {  3 private static int Add(int n, int m)  4  {  5 return n + m;  6  }  7  8 public static async Task<int> AddAsync(int n, int m)  9  { 10 int val = await Task.Run(() => Add(n, m)); 11 12 return val; 13  } 14 }

                                     圖2

                                     圖3

 (2)Task:調用方法不須要從異步方法中取返回值,可是但願檢查異步方法的狀態,那麼能夠選擇能夠返回 Task 類型的對象。不過,就算異步方法中包含 return 語句,也不會返回任何東西。

 1         private static void Main(string[] args)  2  {  3 Task t = Calculator.AddAsync(1, 2);  4  5 //一直在幹活  6  7  t.Wait();  8 Console.WriteLine("AddAsync 方法執行完成");  9 10  Console.Read(); 11 }
 1     internal class Calculator  2  {  3 private static int Add(int n, int m)  4  {  5 return n + m;  6  }  7  8 public static async Task AddAsync(int n, int m)  9  { 10 int val = await Task.Run(() => Add(n, m)); 11 Console.WriteLine($"Result: {val}"); 12  } 13 }

                            圖4

                                      圖5

  (3)void:調用方法執行異步方法,但又不須要作進一步的交互。 

 1         private static void Main(string[] args)  2  {  3 Calculator.AddAsync(1, 2);  4  5 //一直在幹活  6  7 Thread.Sleep(1000); //掛起1秒鐘  8 Console.WriteLine("AddAsync 方法執行完成");  9 10  Console.Read(); 11 }
 1     internal class Calculator  2  {  3 private static int Add(int n, int m)  4  {  5 return n + m;  6  }  7  8 public static async void AddAsync(int n, int m)  9  { 10 int val = await Task.Run(() => Add(n, m)); 11 Console.WriteLine($"Result: {val}"); 12  } 13 }

                                       圖6

                                           圖7

4、控制流

     異步方法的結構可拆分紅三個不一樣的區域:
     (1)表達式以前的部分:從方法頭到第一個 await 表達式之間的全部代碼。
     (2)await 表達式:將被異步執行的代碼。
     (3)表達式以後的部分:await 表達式的後續部分。
 
                                                                                             圖1-1
  該異步方法執行流程:從await表達式以前的地方開始,同步執行到第一個 await,標識着第一部分執行結束,通常來講此時 await 工做還沒完成。當await 任務完成後,該方法將繼續同步執行後續部分。在執行的後續部分中,若是依然存在 await,就重複上述過程。
  當到達 await 表達式時,線程將從異步方法返回到調用方法。若是異步方法的返回類型爲 Task 或 Task<T>,會建立一個 Task 對象,標識須要異步完成的任務,而後將 Task 返回來調用方法。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
                                                    圖1-2
  異步方法的控制流:
  ①異步執行 await 表達式的空閒任務。
  ②await 表達式執行完成,繼續執行後續部分。如再遇到 await 表達式,按相同狀況進行處理。
  ③到達末尾或遇到 return 語句時,根據返回類型能夠分三種狀況:
    a.void:退出控制流。
    b.Task:設置 Task 的屬性並退出。
    c.Task<T>:設置 Task 的屬性和返回值(Result 屬性)並退出。
  ④同時,調用方法將繼續執行,從異步方法獲取 Task 對象。須要值的時候,會暫停等到 Task 對象的 Result 屬性被賦值纔會繼續執行。
 
  【難點】
  ①第一次遇到 await 所返回對象的類型。這個返回類型就是同步方法頭的返回類型,跟 await 表達式的返回值沒有關係。
  ②到達異步方法的末尾或遇到 return 語句,它並無真正的返回一個值,而是退出了該方法。

5、await 表達式

  await 表達式指定了一個異步執行的任務。默認狀況,該任務在當前線程異步執行。

  每個任務就是一個 awaitable 類的實例。awaitable 類型指包含 GetAwaiter() 方法的類型。

  實際上,你並不須要構建本身的 awaitable,通常只須要使用 Task 類,它就是 awaitable。

  最簡單的方式是在方法中使用 Task.Run() 來建立一個 Task。【注意】它是在不一樣的線程上執行方法。

  讓咱們一塊兒來看看示例。

 1     internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 var t = Do.GetGuidAsync();  6  t.Wait();  7  8  Console.Read();  9  } 10 11 12 private class Do 13  { 14 /// <summary> 15 /// 獲取 Guid 16 /// </summary> 17 /// <returns></returns> 18 private static Guid GetGuid() //與Func<Guid> 兼容 19  { 20 return Guid.NewGuid(); 21  } 22 23 /// <summary> 24 /// 異步獲取 Guid 25 /// </summary> 26 /// <returns></returns> 27 public static async Task GetGuidAsync() 28  { 29 var myFunc = new Func<Guid>(GetGuid); 30 var t1 = await Task.Run(myFunc); 31 32 var t2 = await Task.Run(new Func<Guid>(GetGuid)); 33 34 var t3 = await Task.Run(() => GetGuid()); 35 36 var t4 = await Task.Run(() => Guid.NewGuid()); 37 38 Console.WriteLine($"t1: {t1}"); 39 Console.WriteLine($"t2: {t2}"); 40 Console.WriteLine($"t3: {t3}"); 41 Console.WriteLine($"t4: {t4}"); 42  } 43  } 44 }

圖2-1

圖2-2

   上面 4 個 Task.Run() 都是採用了 Task Run(Func<TReturn> func) 形式來直接或間接調用 Guid.NewGuid()。

   Task.Run() 支持 4 中不一樣的委託類型所表示的方法:Action、Func<TResult>、Func<Task> 和 Func<Task<TResult>>

 1    internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 var t = Do.GetGuidAsync();  6  t.Wait();  7  8  Console.Read();  9  } 10 11 private class Do 12  { 13 public static async Task GetGuidAsync() 14  { 15 await Task.Run(() => { Console.WriteLine(Guid.NewGuid()); }); //Action 16 17 Console.WriteLine(await Task.Run(() => Guid.NewGuid())); //Func<TResult> 18 19 await Task.Run(() => Task.Run(() => { Console.WriteLine(Guid.NewGuid()); })); //Func<Task> 20 21 Console.WriteLine(await Task.Run(() => Task.Run(() => Guid.NewGuid()))); //Func<Task<TResult>> 22  } 23  } 24 }

 

 

 

 

 

 

 

 

 

 

圖2-3 Task.Run() 方法的重載

6、How 取消異步操做

   CancellationToken 和 CancellationTokenSource 這兩個類容許你終止執行異步方法。

  (1)CancellationToken 對象包含任務是否被取消的信息;若是該對象的屬性 IsCancellationRequested 爲 true,任務需中止操做並返回;該對象操做是不可逆的,且只能使用(修改)一次,即該對象內的 IsCancellationRequested 屬性被設置後,就不能改動。

  (2)CancellationTokenSource 可建立 CancellationToken 對象,調用 CancellationTokenSource 對象的 Cancel 方法,會使該對象的 CancellationToken 屬性 IsCancellationRequested 設置爲 true。

  【注意】調用 CancellationTokenSource 對象的 Cancel 方法,並不會執行取消操做,而是會將該對象的 CancellationToken 屬性 IsCancellationRequested 設置爲 true。

  示例

 1     internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 CancellationTokenSource source = new CancellationTokenSource();  6 CancellationToken token = source.Token;  7  8 var t = Do.ExecuteAsync(token);  9 10 //Thread.Sleep(3000); //掛起 3 秒 11 //source.Cancel(); //傳達取消請求 12 13 t.Wait(token); //等待任務執行完成 14 Console.WriteLine($"{nameof(token.IsCancellationRequested)}: {token.IsCancellationRequested}"); 15 16  Console.Read(); 17  } 18 19 20  } 21 22 internal class Do 23  { 24 /// <summary> 25 /// 異步執行 26 /// </summary> 27 /// <param name="token"></param> 28 /// <returns></returns> 29 public static async Task ExecuteAsync(CancellationToken token) 30  { 31 if (token.IsCancellationRequested) 32  { 33 return; 34  } 35 36 await Task.Run(() => CircleOutput(token), token); 37  } 38 39 /// <summary> 40 /// 循環輸出 41 /// </summary> 42 /// <param name="token"></param> 43 private static void CircleOutput(CancellationToken token) 44  { 45 Console.WriteLine($"{nameof(CircleOutput)} 方法開始調用:"); 46 47 const int num = 5; 48 for (var i = 0; i < num; i++) 49  { 50 if (token.IsCancellationRequested) //監控 CancellationToken 51  { 52 return; 53  } 54 55 Console.WriteLine($"{i + 1}/{num} 完成"); 56 Thread.Sleep(1000); 57  } 58  } 59 }
複製代碼

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

圖3-1

 

 

 

 

 

 

 

 

 

圖3-2 註釋兩行代碼

 

 

 

 

 

圖3-3:圖3-1和圖3-2的執行結果(註釋兩行代碼)

上圖是不調用 Cancel() 方法的結果圖,不會取消任務的執行。

下圖在 3 秒後調用 Cancel() 方法取消任務的執行:

 

 

 

 

 

 

 

 

 

圖3-4:去掉註釋

 

 

 

 

圖3-5:圖3-1和圖3-4的執行結果(去掉註釋)

7、異常處理

  await 表達式也可使用 try...catch...finally 結構。

 1     internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 var t = DoExceptionAsync();  6  t.Wait();  7  8 Console.WriteLine($"{nameof(t.Status)}: {t.Status}"); //任務狀態  9 Console.WriteLine($"{nameof(t.IsCompleted)}: {t.IsCompleted}"); //任務完成狀態標識 10 Console.WriteLine($"{nameof(t.IsFaulted)}: {t.IsFaulted}"); //任務是否有未處理的異常標識 11 12  Console.Read(); 13  } 14 15 /// <summary> 16 /// 異常操做 17 /// </summary> 18 /// <returns></returns> 19 private static async Task DoExceptionAsync() 20  { 21 try 22  { 23 await Task.Run(() => { throw new Exception(); }); 24  } 25 catch (Exception) 26  { 27 Console.WriteLine($"{nameof(DoExceptionAsync)} 出現異常!"); 28  } 29  } 30 }
複製代碼

 

 

 

 

  圖1-1

【分析】await 表達式位於 try 塊中,按普通的方式處理異常。可是,爲何圖中的狀態(Status)、是否完成標識(IsCompleted)和是否失敗標識(IsFaulted)分別顯示:運行完成(RanToCompletion) 、已完成(True) 和 未失敗(False) 呢?由於:任務沒有被取消,而且異常都已經處理完成!

8、在調用方法中同步等待任務

  調用方法可能在某個時間點上須要等待某個特殊的 Task 對象完成,才執行後面的代碼。此時,能夠採用實例方法 Wait 。

 1     internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 var t = CountCharactersAsync("http://www.cnblogs.com/liqingwen/");  6  7 t.Wait(); //等待任務結束  8 Console.WriteLine($"Result is {t.Result}");  9 10  Console.Read(); 11  } 12 13 /// <summary> 14 /// 統計字符數量 15 /// </summary> 16 /// <param name="address"></param> 17 /// <returns></returns> 18 private static async Task<int> CountCharactersAsync(string address) 19  { 20 var result = await Task.Run(() => new WebClient().DownloadStringTaskAsync(address)); 21 return result.Length; 22  } 23 }

 

 

圖2-1

 Wait() 適合用於單一 Task 對象,若是想操做一組對象,可採用 Task 的兩個靜態方法 WaitAll() 和 WaitAny() 。

複製代碼
 1     internal class Program  2  {  3 private static int time = 0;  4 private static void Main(string[] args)  5  {  6 var t1 = GetRandomAsync(1);  7 var t2 = GetRandomAsync(2);  8  9 //IsCompleted 任務完成標識 10 Console.WriteLine($"t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}"); 11 Console.WriteLine($"t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}"); 12 13  Console.Read(); 14  } 15 16 /// <summary> 17 /// 獲取一個隨機數 18 /// </summary> 19 /// <param name="id"></param> 20 /// <returns></returns> 21 private static async Task<int> GetRandomAsync(int id) 22  { 23 var num = await Task.Run(() => 24  { 25 time++; 26 Thread.Sleep(time * 100); 27 return new Random().Next(); 28  }); 29 30 Console.WriteLine($"{id} 已經調用完成"); 31 return num; 32  } 33 }
複製代碼

 

 

 

圖2-2 兩個任務的 IsCompleted 屬性都顯示未完成

如今,在 Main() 方法中新增兩行代碼(6 和 7 兩行),嘗試調用 WaitAll() 方法。

複製代碼
 1        private static void Main(string[] args)  2  {  3 var t1 = GetRandomAsync(1);  4 var t2 = GetRandomAsync(2);  5  6 Task<int>[] tasks = new Task<int>[] { t1, t2 };  7 Task.WaitAll(tasks); //等待任務所有完成,才繼續執行  8  9 //IsCompleted 任務完成標識 10 Console.WriteLine($"t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}"); 11 Console.WriteLine($"t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}"); 12 13  Console.Read(); 14 }
複製代碼

 

 

 

 

圖2-3 兩個任務的 IsCompleted 屬性都顯示 True

如今,再次將第 7 行改動一下,調用 WaitAny() 方法試試。 

複製代碼
 1         private static void Main(string[] args)  2  {  3 var t1 = GetRandomAsync(1);  4 var t2 = GetRandomAsync(2);  5  6 Task<int>[] tasks = new Task<int>[] { t1, t2 };  7 Task.WaitAny(tasks); //等待任一 Task 完成,才繼續執行  8  9 //IsCompleted 任務完成標識 10 Console.WriteLine($"t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}"); 11 Console.WriteLine($"t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}"); 12 13  Console.Read(); 14 }
複製代碼

 

 

 

圖2-4 有一個任務的 IsCompleted 屬性顯示 True (完成) 就繼續執行

9、在異步方法中異步等待任務

  上節說的是如何使用 WaitAll() 和 WaitAny() 同步地等待 Task 完成。此次咱們使用 Task.WhenAll() 和 Task.WhenAny()  在異步方法中異步等待任務。

複製代碼
 1   internal class Program  2  {  3 private static int time = 0;  4  5 private static void Main(string[] args)  6  {  7 var t = GetRandomAsync();  8  9 Console.WriteLine($"t.{nameof(t.IsCompleted)}: {t.IsCompleted}"); 10 Console.WriteLine($"Result: {t.Result}"); 11 12  Console.Read(); 13  } 14 15 /// <summary> 16 /// 獲取一個隨機數 17 /// </summary> 18 /// <param name="id"></param> 19 /// <returns></returns> 20 private static async Task<int> GetRandomAsync() 21  { 22 time++; 23 var t1 = Task.Run(() => 24  { 25 Thread.Sleep(time * 100); 26 return new Random().Next(); 27  }); 28 29 time++; 30 var t2 = Task.Run(() => 31  { 32 Thread.Sleep(time * 100); 33 return new Random().Next(); 34  }); 35 36 //異步等待集合內的 Task 都完成,才進行下一步操做 37 await Task.WhenAll(new List<Task<int>>() { t1, t2 }); 38 39 Console.WriteLine($" t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}"); 40 Console.WriteLine($" t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}"); 41 42 return t1.Result + t2.Result; 43  } 44 }

 

 

 

圖3-1 調用 WhenAll()  方法

【注意】WhenAll() 異步等待集合內的 Task 都完成,不會佔用主線程的時間。

如今,咱們把 GetRandomAsync() 方法內的 WhenAll() 方法替換成 WhenAny(),而且增大一下線程掛起時間,最終改動以下:

複製代碼
 1    private static async Task<int> GetRandomAsync()  2  {  3 time++;  4 var t1 = Task.Run(() =>  5  {  6 Thread.Sleep(time * 100);  7 return new Random().Next();  8  });  9 10 time++; 11 var t2 = Task.Run(() => 12  { 13 Thread.Sleep(time * 500); //這裏由 100 改成 500,否則看不到效果 14 return new Random().Next(); 15  }); 16 17 //異步等待集合內的 Task 都完成,才進行下一步操做 18 //await Task.WhenAll(new List<Task<int>>() { t1, t2 }); 19 await Task.WhenAny(new List<Task<int>>() { t1, t2 }); 20 21 Console.WriteLine($" t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}"); 22 Console.WriteLine($" t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}"); 23 24 return t1.Result + t2.Result; 25 }
複製代碼

 

 

 

圖3-2 調用 WhenAny() 方法

10、Task.Delay() 暫停執行

  Task.Delay() 方法會建立一個 Task 對象,該對象將暫停其在線程中的處理,並在必定時間以後完成。和 Thread.Sleep 不一樣的是,它不會阻塞線程,意味着線程能夠繼續處理其它工做。

 1     internal class Program  2  {  3 private static void Main(string[] args)  4  {  5 Console.WriteLine($"{nameof(Main)} - start.");  6  DoAsync();  7 Console.WriteLine($"{nameof(Main)} - end.");  8  9  Console.Read(); 10  } 11 12 private static async void DoAsync() 13  { 14 Console.WriteLine($" {nameof(DoAsync)} - start."); 15 16 await Task.Delay(500); 17 18 Console.WriteLine($" {nameof(DoAsync)} - end."); 19  } 20 }

相關文章
相關標籤/搜索