前言
最近在學習Web Api框架的時候接觸到了async/await,這個特性是.NET 4.5引入的,由於之前對於非同步程式設計不是很瞭解,所以花費了一些時間學習一下相關的知識,並整理成這篇部落格,如果在閱讀的過程中發現不對的地方,歡迎大家指正。
同步程式設計與非同步程式設計
通常情況下,我們寫的C#程式碼就是同步的,執行在同一個執行緒中,從程式的第一行程式碼到最後一句程式碼順序執行。而非同步程式設計的核心是使用多執行緒,通過讓不同的執行緒執行不同的任務,實現不同程式碼的並行執行。
前臺執行緒與後臺執行緒
關於多執行緒,早在.NET2.0時代,基礎類庫中就提供了Thread實現。預設情況下,例項化一個Thread建立的是前臺執行緒,只要有前臺執行緒在執行,應用程式的程式就一直處於執行狀態,以控制檯應用程式為例,在Main方法中例項化一個Thread,這個Main方法就會等待Thread執行緒執行完畢才退出。而對於後臺執行緒,應用程式將不考慮其是否執行完畢,只要應用程式的主執行緒和前臺執行緒執行完畢就可以退出,退出後所有的後臺執行緒將被自動終止。來看程式碼應該更清楚一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp { class Program { static void Main(string[] args) { Console.WriteLine("主執行緒開始"); //例項化Thread,預設建立前臺執行緒 Thread t1 = new Thread(DoRun1); t1.Start(); //可以通過修改Thread的IsBackground,將其變為後臺執行緒 Thread t2 = new Thread(DoRun2) { IsBackground = true }; t2.Start(); Console.WriteLine("主執行緒結束"); } static void DoRun1() { Thread.Sleep(500); Console.WriteLine("這是前臺執行緒呼叫"); } static void DoRun2() { Thread.Sleep(1500); Console.WriteLine("這是後臺執行緒呼叫"); } } } |
執行上面的程式碼,可以看到DoRun2方法的列印資訊“這是後臺執行緒呼叫”將不會被顯示出來,因為應用程式執行完主執行緒和前臺執行緒後,就自動退出了,所有的後臺執行緒將被自動終止。這裡後臺執行緒設定了等待1.5s,假如這個後臺執行緒比前臺執行緒或主執行緒提前執行完畢,對應的資訊“這是後臺執行緒呼叫”將可以被成功列印出來。
Task
.NET 4.0推出了新一代的多執行緒模型Task。async/await特性是與Task緊密相關的,所以在瞭解async/await前必須充分了解Task的使用。這裡將以一個簡單的Demo來看一下Task的使用,同時與Thread的建立方式做一下對比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主執行緒啟動"); //.NET 4.5引入了Task.Run靜態方法來啟動一個執行緒 Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1啟動"); }); //Task啟動的是後臺執行緒,假如要在主執行緒中等待後臺執行緒執行完畢,可以呼叫Wait方法 Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine("Task2啟動"); }); task.Wait(); Console.WriteLine("主執行緒結束"); } } } Task的使用 |
首先,必須明確一點是Task啟動的執行緒是後臺執行緒,不過可以通過在Main方法中呼叫task.Wait()方法,使應用程式等待task執行完畢。Task與Thread的一個重要區分點是:Task底層是使用執行緒池的,而Thread每次例項化都會建立一個新的執行緒。這裡可以通過這段程式碼做一次驗證:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void DoRun1() { Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void DoRun2() { Thread.Sleep(50); Console.WriteLine("Task呼叫Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void Main(string[] args) { for (int i = 0; i < 50; i++) { new Thread(DoRun1).Start(); } for (int i = 0; i < 50; i++) { Task.Run(() => { DoRun2(); }); } //讓應用程式不立即退出 Console.Read(); } } } Task底層使用執行緒池 |
執行程式碼,可以看到DoRun1()方法每次的Thread Id都是不同的,而DoRun2()方法的Thread Id是重複出現的。我們知道執行緒的建立和銷燬是一個開銷比較大的操作,Task.Run()每次執行將不會立即建立一個新執行緒,而是到CLR執行緒池檢視是否有空閒的執行緒,有的話就取一個執行緒處理這個請求,處理完請求後再把執行緒放回執行緒池,這個執行緒也不會立即撤銷,而是設定為空閒狀態,可供執行緒池再次排程,從而減少開銷。
Task<TResult>
Task<TResult>是Task的泛型版本,這兩個之間的最大不同是Task<TResult>可以有一個返回值,看一下程式碼應該一目瞭然:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主執行緒開始"); Task<string> task = Task<string>.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); Console.WriteLine(task.Result); Console.WriteLine("主執行緒結束"); } } } Task<TResult>的使用 |
Task<TResult>的例項物件有一個Result屬性,當在Main方法中呼叫task.Result的時候,將等待task執行完畢並得到返回值,這裡的效果跟呼叫task.Wait()是一樣的,只是多了一個返回值。
async/await 特性
經過前面的鋪墊,終於迎來了這篇文章的主角async/await,還是先通過程式碼來感受一下這兩個特性的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主執行緒啟動-------"); Task<int> task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主執行緒結束-------"); } static async Task<int> GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); string str = await GetStringAsync(); Console.WriteLine("GetLengthAsync End"); return str.Length; } static Task<string> GetStringAsync() { return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; }); } } } async/await 用法 |
首先來看一下async關鍵字。async用來修飾方法,表明這個方法是非同步的,宣告的方法的返回型別必須為:void或Task或Task<TResult>。返回型別為Task的非同步方法中無需使用return返回值,而返回型別為Task<TResult>的非同步方法中必須使用return返回一個TResult的值,如上述Demo中的非同步方法返回一個int。
再來看一下await關鍵字。await必須用來修飾Task或Task<TResult>,而且只能出現在已經用async關鍵字修飾的非同步方法中。
通常情況下,async/await必須成對出現才有意義,假如一個方法宣告為async,但卻沒有使用await關鍵字,則這個方法在執行的時候就被當作同步方法,這時編譯器也會丟擲警告提示async修飾的方法中沒有使用await,將被作為同步方法使用。瞭解了關鍵字async\await的特點後,我們來看一下上述Demo在控制檯會輸入什麼吧。
輸出的結果已經很明確地告訴我們整個執行流程了。GetLengthAsync非同步方法剛開始是同步執行的,所以”GetLengthAsync Start”字串會被列印出來,直到遇到第一個await關鍵字,真正的非同步任務GetStringAsync開始執行,await相當於起到一個標記/喚醒點的作用,同時將控制權放回給Main方法,”Main方法做其他事情”字串會被列印出來。之後由於Main方法需要訪問到task.Result,所以就會等待非同步方法GetLengthAsync的執行,而GetLengthAsync又等待GetStringAsync的執行,一旦GetStringAsync執行完畢,就會回到await GetStringAsync這個點上執行往下執行,這時”GetLengthAsync End”字串就會被列印出來。
當然,我們也可以使用下面的方法完成上面控制檯的輸出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主執行緒啟動-------"); Task<int> task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主執行緒結束-------"); } static Task<int> GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); Task<int> task = Task<int>.Run(() => { string str = GetStringAsync().Result; Console.WriteLine("GetLengthAsync End"); return str.Length; }); return task; } static Task<string> GetStringAsync() { return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; }); } } } 不使用async\await |
對比兩種方法,是不是async\await關鍵字的原理其實就是通過使用一個執行緒完成非同步呼叫嗎?答案是否定的。async關鍵字表明可以在方法內部使用await關鍵字,方法在執行到await前都是同步執行的,執行到await處就會掛起,並返回到Main方法中,直到await標記的Task執行完畢,才喚醒回到await點上,繼續向下執行。更深入點的介紹可以檢視文章末尾的參考文獻。
async/await 實際應用
微軟已經對一些基礎類庫的方法提供了非同步實現,接下來將實現一個例子來介紹一下async/await的實際應用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; using System.Net; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("開始獲取部落格園首頁字元數量"); Task<int> task1 = CountCharsAsync("http://www.cnblogs.com"); Console.WriteLine("開始獲取百度首頁字元數量"); Task<int> task2 = CountCharsAsync("http://www.baidu.com"); Console.WriteLine("Main方法中做其他事情"); Console.WriteLine("部落格園:" + task1.Result); Console.WriteLine("百度:" + task2.Result); } static async Task<int> CountCharsAsync(string url) { WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(new Uri(url)); return result.Length; } } } Demo |
參考文獻:<IIIustrated C# 2012> 關於async/await的FAQ