上一章我簡單介紹了異步編程的基本方法,推薦使用的方式是Task。Task是對線程池的封裝,並且可以對Task使用async和await關鍵字。這兩個關鍵字的使用非常簡單,那么這兩個關鍵字究竟起什么作用?工作原理是怎樣的?本文就來簡單解釋。
本系列是我讀《CLR via C#》的總結,但是書中關於async和await關鍵字的講解不是很多。其中28.3小節通過簡單例子以及IL反編譯的方式,講解了編譯器如何將異步函數編譯成狀態機,雖然反編譯出的代碼中作者添加了大量的注釋,無奈本人能力有限,很多底層代碼不能很好的理解,因此我通過小例子的方式反復的嘗試,加上書中的講解,我想我已經基本掌握了async和await的使用方式和基本原理。若您有興趣了解底層代碼,可以找來《CLR via C#》的28.2和28.3章節來讀一讀。
上一章節已經講了,當你想要利用另一個線程來執行一段程序,推薦的方式是Task,我們看個例子:
public async void RunAsync(){ var t = await Task.Run(() => {
//模擬其他操作 Thread.Sleep(2000); return "task finished"; }); Console.WriteLine(t); }
運行代碼,可以看到程序在大約2秒后,控制台會打印出"task finished",且在這過程中,主線程沒有被Sleep(阻塞),RunAsync后面的方法可以繼續執行,如
1 static void Main(string[] args) { 2 RunAsync(); 3 Console.WriteLine("123"); 4 Console.Read(); 5 }
控制台會先顯示123,過大約2秒后,顯示task finished。程序在執行RunAsync()后,跳過了該方法,直接執行了Console.WriteLine("123")。可以看到RunAsync方法的簽名中添加了async關鍵字,在Task.Run()前面添加了await關鍵字,這兩個關鍵字的作用是表示RunAsync方法在執行到await關鍵字后,會將該方法的其余部分封裝成一個委托,該委托會在Task.Run()返回的task執行完成后,執行該委托(具體編譯器如何將該方法轉換成狀態機,並在任務結束后,再繼續執行該委托,可以看《CLR via C#》的28.3章節)。
1 public async void RunAsync(){ 2 //其他操作 3 //當遇到await,會將后面的程序封裝成一個委托 4 var t = await Task.Run(() => 5 { 6 Thread.Sleep(2000); 7 return "task finished"; 8 }); 9 //這里往后的代碼被封裝成委托,當t.IsComplete后,才會被執行 10 //其他操作 11 Console.WriteLine(t); 12 }
關鍵字async表明該方法是一個異步方法,await關鍵字只允許在標有async的方法中使用。當異步方法具有返回值時,調用該異步方法的函數也要添加async關鍵字,並在調用方法處添加await,不然會造成異步失效。
1 static void Main(string[] args){ 2 Console.WriteLine(RunAsync().Result); 3 Console.WriteLine("Async Run"); 4 Console.Read(); 5 } 6 public static async Task<string> RunAsync(){ 7 return await Task.Run(() => 8 { 9 Thread.Sleep(2000); 10 return "task finished"; 11 }); 12 }
運行,程序在等待大約2秒后,顯示task finished,然后再顯示Async Run,這是因為雖然RunAsync方法異步返回,但是主線程一直在等待RunAsync的結果,除此之外什么也不干,這樣當然是不好。
1 static void Main(string[] args){ 2 TestAsync(); 3 Console.WriteLine("Async Run"); 4 Console.Read(); 5 } 6 public static async Task<string> RunAsync(){ 7 return await Task.Run(() => 8 { 9 Thread.Sleep(2000); 10 return "task finished"; 11 }); 12 } 13 public static async void TestAsync(){ 14 Console.WriteLine(await RunAsync()); 15 }
Main函數無法添加async關鍵字,因此我用TestAsync方法包裝了一下,可以看到添加了await 和async關鍵字后,程序會先出現Async Run,然后是task finished。即在異步函數的調用中,若函數包含返回值,則應通過添加await的方式調用異步函數,這樣可以做到接着異步,而不是半途而廢的異步。
在循環中也可以添加await關鍵字
1 static void Main(string[] args) 2 { 3 TestAsync(); 4 Console.WriteLine("Async Run"); 5 Console.Read(); 6 } 7 public static async Task<string> RunAsync() 8 { 9 return await Task.Run(() => 10 { 11 Thread.Sleep(2000); 12 return "task finished"; 13 }); 14 } 15 public static async void TestAsync() 16 { 17 for (int i = 0; i < 4; i++) 18 { 19 Console.WriteLine(await RunAsync() + i); 20 } 21 }
上述例子會以你希望的那樣,先顯示Async Run,然后依次打印task finished0 - task finished4,且每次打印間歇大約2秒。
以上就是Task與 async 和 await 關鍵字的使用以及基本原理。使用await關鍵字,該關鍵字之后的邏輯都會被封裝到一個委托,等到任務執行結束后,再調用當前線程繼續執行該委托。那么能夠調用當前線程,也應該有方法當任務執行結束后,繼續調用線程池來執行方法。該部分C#多線程編程(3)會有講解。歡迎有問題的小伙伴和我在評論區交流。