大部分人可能已經知道,Visual Studio 11引入了新的“async”和“await”。這是另外一篇介紹文章。
(譯者注:本文寫於 2012 年)
首先,點睛之筆:async 會從根本上改變我們大部分程式碼的編寫方式。
是的,我相信async、await會帶來比LINQ更大的影響。在很短時間以後,理解 async 會變成一個基本需求。
關鍵字介紹
讓我們開始吧。我會使用一些在後面才會詳細說明的內容——在第一部分中請堅持一下。
非同步方法看上去就像這樣:
1 2 3 4 5 6 |
public async Task DoSomethingAsync() { // 在真實世界裡,我們會真的去執行一些操作。。。 // 但對於這個示例,我們只是簡單的(非同步地)等待100毫秒。 await Task.Delay(100); } |
“async”這個關鍵字讓我們能夠在方法內部使用“await”關鍵字,並且改變了處理結果的方式,這就是async關鍵字做的全部內容!我們並沒有線上程池的執行緒中執行這個方法,也沒有使用任何其它魔法。async關鍵字只是啟用了await關鍵字(並管理方法結果)。
非同步方法在開始執行時,和其它任何方法都是一樣的。也就是說,在遇到“await”關鍵字(或者丟擲異常)之前,方法都是同步執行的。
“await”關鍵字可以讓事情非同步執行。await就像一元操作符。它接收一個單獨的引數:可等待(可等待是一個非同步操作)。await會檢查可等待操作是否已經結束,如果可等待操作已經完成,方法就會繼續執行(就像一個正常的同步執行方法)。
如果“await”發現可等待操作還沒有完成,那麼就會非同步地執行。它會告訴可執行操作,在完成之後,繼續執行方法剩餘的部分,然後從非同步方法返回。
過些時候,當可執行操作完成後,它會執行非同步方法的剩餘部分。如果你在等待一個內建的可等待操作(例如Task),那麼非同步方法的剩餘部分會在“await”返回之前的“上下文”中執行。
我喜歡將“await”當做“非同步等待”。也就是說,非同步方法會暫停,直到可等待操作結束(因此它在等待),但實際上執行緒並不會被阻塞(因此它是非同步的)。
可等待
如我所說,“await”關鍵字使用一個單獨的引數——“可等待的”,一個非同步操作。在.NET框架中,我們有兩個常用的可等待型別:Task<T> 和 Task。
我們也有一些其它的可等待型別:有一些特殊方法,例如”Task.Yield”會返回一個可等待的值,但這個值不是Task;對於WinRT執行時(隨Windows 8而來)會有一個非託管的可等待型別。你也可以建立自己的可等待型別(通常是為了效能的原因),或者使用擴充套件方法將一個不可等待的型別變為可等待型別。
這就是我想說的,如何建立你自己的可等待型別。在使用async/await的全部時間裡,我不得不建立了幾個可等待型別。如果想了解更多關於建立自定義可等待型別的資訊,你可以檢視 Parallel團隊的部落格 或者 Jon Skeet的部落格。
關於可等待型別,有一點很重要:可等待指的是型別,而不是返回該型別的方法。換句話說,你可以等待一個返回 Task 的方法的結果,這是因為這個方法返回的是Task,而不是因為方法是非同步的。這樣你也可以等待一個返回 Task 非非同步方法的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public async Task NewStuffAsync() { //使用await關鍵字,希望你能玩得開心。 await ... } public Task MyOldTaskParallelLibraryCode() { // 請注意這不是一個非同步方法,因此我們不能夠在這裡使用await關鍵字。 ... } public async Task ComposeAsync() { // 我們可以等待任務,而無需關心它們來自何處。 await NewStuffAsync(); await MyOldTaskParallelLibraryCode(); } |
小貼士:如果你有一個非常簡單的非同步方法,你可能根本不會使用 await 關鍵字(例如,使用Task.FromResult)。如果你可以不使用 await 關鍵字,那麼你就不應該使用它,並將 async 關鍵字從方法宣告中移除。一個返回 Task.FromResult 的非非同步方法要比一個返回正常值的非同步方法更有效率。
返回型別
非同步方法可以返回Task<T>、Task或者void。在大多數情況下,你可能會希望返回 Task<T> 或者 Task,只有萬不得已時,才會返回void。
為什麼返回 Task<T> 或者 Task 呢?因為它們是可等待的,而void不是。這樣當你有一個返回 Task<T> 或者 Task 的非同步方法時,你就可以將結果傳遞給 await。如果使用void方法,你不能向await傳遞任何值。
當有非同步的事件處理方法時,你只能返回void。
你也可以在其他“高階別”行為上使用非同步void方法——例如,針對控制檯程式的單獨的“static async void MainAsync()”。然而,非同步void方法有自身的問題,可以參考非同步控制檯程式。非同步void方法的主要還是用於事件處理。
返回值
如果一個非同步方法返回 Task 或者 void,那麼它不會有返回值,如果一個非同步方法返回 Task<T>,那麼它必須返回一個型別為 T 的值:
1 2 3 4 5 6 7 |
public async Task<int> CalculateAnswer() { await Task.Delay(100); // (Probably should be longer...) // 返回一個int型別,而不是Task<int>。 return 42; } |
這裡有點兒奇怪,我們需要去適應。但這樣設計的背後,還是有一些“好的理由”的。
上下文
在概述中,我提到過,如果你在等待一個內建的可等待操作,那麼可等待操作會捕捉當前的“上下文”,並在後面執行非同步方法剩餘部分時,使用該“上下文”。那麼“上下文”到底是什麼?
簡單的回答是:
- 如果你是在UI執行緒上,那麼它就是UI上下文。
- 如果你是在響應一個ASP.NET的請求,那麼它就是ASP.NET的請求上下文。
- 否則,通常它是一個執行緒池上下文。
複雜的回答:
- 如果 SynchronizationContext.Current 不為空,那麼它是當前的 SynchronizationContext(UI上下文和ASP.NET請求上下文都是 SynchronizationContext 上下文)。
- 否則,它是當前的 TaskScheduler(TaskScheduler.Default是執行緒池上下文)。
那麼在實際中這意味著什麼?首先,捕捉(以及儲存)UI、ASP.NET上下文是透明的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// WinForms示例(對於WPF來說是一樣的) private async void DownloadFileButton_Click(object sender, EventArgs e) { // 既然我們使用非同步的方式等待,那麼UI執行緒就不會被檔案下載所阻塞。 await DownloadFileAsync(fileNameTextBox.Text); // 既然我們從UI上下文中恢復,我們就可以直接訪問UI元素。 resultTextBox.Text = "File downloaded!"; } // ASP.NET示例 protected async void MyButton_Click(object sender, EventArgs e) { // 既然我們使用非同步方法來等待,那麼ASP.NET執行緒就不會被檔案下載所阻塞。 // 這樣當我們等待的時候,ASP.NET執行緒就可以處理其他的請求。 await DownloadFileAsync(...); // 既然我們從ASP.NET上下文中恢復,我們就可以訪問當前的請求。 // 我們可能實際上是在另外一個*執行緒*上,但我們有相同的ASP.NET請求上下文。 Response.Write("File downloaded!"); } |
對於事件處理來說,這是很棒的,但對於你要寫的其它大部分程式碼來說(實際上,這是你將會寫的大部分非同步程式碼),這可能並不是你想要的。
避免上下文
很多時候,你可能並不需要非同步回到“主”上下文。對於大部分非同步方法來說,在設計時會在頭腦中考慮組合:它們會等待其它操作,每個操作本身都代表一個非同步操作(這意味著,它們可以和其它操作組合在一起)。在這種情況下,你可以通過呼叫 ConfigureAwait 方法並傳入false的方式,來告訴“等待者”不要捕捉當前的上下文。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private async Task DownloadFileAsync(string fileName) { // 使用HttpClient或者其他任何方式來下載檔案內容 var fileContents = await DownloadFileContentsAsync(fileName).ConfigureAwait(false); // 請注意由於ConfigureAwait(false),我們並沒有在原來的上下文中。 // 相反,我們執行線上程池上。 // 將檔案內容寫入到外部的磁碟檔案中。 await WriteToDiskAsync(fileName, fileContents).ConfigureAwait(false); // 這裡第二次呼叫ConfigureAwait(false)並不是*必需*的,但這是一個最佳實踐。 } // WinForms例項(對於WPF來說是一樣的) private async void DownloadFileButton_Click(object sender, EventArgs e) { // 因為我們使用非同步方式來等待,UI執行緒就不會被檔案下載所阻塞。 await DownloadFileAsync(fileNameTextBox.Text); // 因為我們從UI上下文中恢復了, 我們就可以直接訪問UI元素了。 resultTextBox.Text = "File downloaded!"; } |
在這個示例中, 有一點非常重要:每個“級別”上的非同步方法只會呼叫它自己的上下文。DownloadFileButton_Click 在 UI 上下文中啟動,然後呼叫 DownloadFileAsync 方法。DownloadFileAsync 也在 UI 上下文中啟動,但它通過呼叫 ConfigureAwait(false) 方法將上下文丟棄。DownloadFileAsync 方法剩餘部分會執行線上程池上下文中。然而,當 DownloadFileAsync 方法結束後,DownloadButton_Click 方法會恢復執行,它確實會恢復 UI 上下文。
一種好的做法:如果你不知道你確實需要上下文,那麼就用 ConfigureAwait(false) 方法。
非同步組合
到目前為止,我們只考慮了連續組合:非同步方法一次只會等待一個操作。我們也可以啟動多個操作,並等待其中一個(或者全部)結束。為此,我們可以啟動這些操作,但到後面再等待它們:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public async Task DoOperationsConcurrentlyAsync() { Task[] tasks = new Task[3]; tasks[0] = DoOperation0Async(); tasks[1] = DoOperation1Async(); tasks[2] = DoOperation2Async(); // 在這裡,所有的三個任務會同時執行。 // 現在我們等待所有的任務。 await Task.WhenAll(tasks); } public async Task<int> GetFirstToRespondAsync() { // 呼叫兩個Web服務;然後讀取第一個響應。 Task<int>[] tasks = new[] { WebService1Async(), WebService2Async() }; // 等待第一個Web服務響應。 Task<int> firstTask = await Task.WhenAny(tasks); // 返回結果。 return await firstTask; } |
通過使用併發組合(Task.WhenAll 或者 Task.WhenAny),你可以執行簡單的併發操作。你也可以將這些方法和 Task.Run 一起使用,來執行簡單的平行計算。然而,這種方式並不適用於任務並行庫(Task 並行庫)——對於任何CPU密集型的高階並行操作來說,都應該使用 TPL 完成。
使用指南
請檢視“基於任務的非同步模式(TAP)文件”。這份文件寫得非常好,它包含了API設計方面的指導原則,以及如何正確使用async、await(包括取消以及進度報告)。
現在有很多適用await的新技術,我們應該使用它們,而不是那些舊的技術。如果你還在新的非同步程式碼中使用任何下面列出的舊示例,那麼你就做錯了:
舊技術 | 新技術 | 描述 |
---|---|---|
task.Wait | await task | Wait/await for a task to complete 等待一個任務結束 |
task.Result | await task | Get the result of a completed task 獲取已結束任務的結果 |
Task.WaitAny | await Task.WhenAny | Wait/await for one of a collection of tasks to complete 等待任務集中的任何一個結束 |
Task.WaitAll | await Task.WhenAll | Wait/await for every one of a collection of tasks to complete 等待任務集中的全部任務都結束 |
Thread.Sleep | await Task.Delay | Wait/await for a period of time 等待一段時間 |
Task constructor | Task.Run or TaskFactory.StartNew | Create a code-based task 建立一個基於程式碼的任務 |
下一步
我已經在MSDN中發表了一篇文章:非同步程式設計的最佳實踐,這篇文章進一步解釋了“避免使用非同步void”、“自始至終使用非同步”以及“配置上下文”的指導原則。
MSDN 官方文件非常不錯,它包含了“基於任務的非同步模式文件”的線上版本,這個文件非常好,它討論了非同步方法的設計。
非同步團隊發表了“async/await FAQ“,我們可以很好地通過它來繼續學習非同步。在這裡面還包含了最好的部落格文章和視訊的連結。另外,Stephen Toub寫的任何文章都非常有啟發性。
當然,另外一個資源就是我自己的部落格。
我在“Concurrency Cookbook”中包含了大量關於使用 async 和 await 的用例,同時也包含了一些你應該使用任務並行庫(TPL)、Rx或者TPL資料流的情況。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式