這是學習異步編程的入門篇。html
涉及 C# 5.0 引入的 async/await,但在控制檯輸出示例時常常會採用 C# 6.0 的 $"" 來拼接字符串,至關於string.Format() 方法。數據庫
啓動程序時,系統會在內存中建立一個新的進程。進程是構成運行程序資源的集合。編程
在進程內部,有稱爲線程的內核對象,它表明的是真正的執行程序。系統會在 Main 方法的第一行語句就開始線程的執行。服務器
線程:網絡
①默認狀況,一個進程只包含一個線程,從程序的開始到執行結束;dom
②線程能夠派生自其它線程,因此一個進程能夠包含不一樣狀態的多個線程,來執行程序的不一樣部分;異步
③一個進程中的多個線程,將共享該進程的資源;async
④系統爲處理器執行所規劃的單元是線程,而非進程。異步編程
通常來講咱們寫的控制檯程序都只使用了一個線程,從第一條語句按順序執行到最後一條。但在不少的狀況下,這種簡單的模型會在性能或用戶體驗上很差。性能
例如:服務器要同時處理來自多個客戶端程序的請求,又要等待數據庫和其它設備的響應,這將嚴重影響性能。程序不該該將時間浪費在響應上,而要在等待的同時執行其它任務!
如今咱們開始進入異步編程。在異步程序中,代碼不須要按照編寫時的順序執行。這時咱們須要用到 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> 對象取結果,若是尚未結果的話,將阻塞,直有結果返回爲止。
先解析一下專業名詞:
同步方法:一個程序調用某個方法,等到其執行完成以後才進行下一步操做。這也是默認的形式。
異步方法:一個程序調用某個方法,在處理完成以前就返回該方法。經過 async/await 咱們就能夠實現這種類型的方法。
async/await 結構可分紅三部分:
(1)調用方法:該方法調用異步方法,而後在異步方法執行其任務的時候繼續執行;
(2)異步方法:該方法異步執行工做,而後馬上返回到調用方法;
(3)await 表達式:用於異步方法內部,指出須要異步執行的任務。一個異步方法能夠包含多個 await 表達式(不存在 await 表達式的話 IDE 會發出警告)。
如今咱們來分析一下示例。
圖2-1
異步方法:在執行完成前當即返回調用方法,在調用方法繼續執行的過程當中完成任務。
語法分析:
(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.解析了進程和線程的概念
2.異步的簡單用法
3.async/await 結構體
4.異步方法語法結構
這是上篇《走進異步編程的世界 - 開始接觸 async/await 異步編程》(入門)的第二章內容,主要是與你們共同深刻探討下異步方法。
本文要求瞭解委託的使用。
異步方法:在執行完成前當即返回調用方法,在調用方法繼續執行的過程當中完成任務。
語法分析:
(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 }
Program.cs
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 }
View Code
圖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 }
Program.cs
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 }
View Code
圖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 }
Program.cs
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 }
Calculator.cs
圖6
圖7
異步方法的結構可拆分紅三個不一樣的區域:
(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 語句,它並無真正的返回一個值,而是退出了該方法。
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 }
View Code
圖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 }
View Code
圖2-3 Task.Run() 方法的重載
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 }
View Code
圖3-1
圖3-2 註釋兩行代碼
圖3-3:圖3-1和圖3-2的執行結果(註釋兩行代碼)
上圖是不調用 Cancel() 方法的結果圖,不會取消任務的執行。
下圖在 3 秒後調用 Cancel() 方法取消任務的執行:
圖3-4:去掉註釋
圖3-5:圖3-1和圖3-4的執行結果(去掉註釋)
感謝你們的支持,這是昨天發佈《走進異步編程的世界 - 剖析異步方法(上)》的補充篇。
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) 呢?由於:任務沒有被取消,而且異常都已經處理完成!
調用方法可能在某個時間點上須要等待某個特殊的 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 (完成) 就繼續執行
上節說的是如何使用 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() 方法
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 }
圖4-1