Async 和 Await

Wing發表於2015-12-11

大部分人可能已經知道,Visual Studio 11引入了新的“async”和“await”。這是另外一篇介紹文章。

(譯者注:本文寫於 2012 年)

首先,點睛之筆:async 會從根本上改變我們大部分程式碼的編寫方式。

是的,我相信async、await會帶來比LINQ更大的影響。在很短時間以後,理解 async 會變成一個基本需求。

關鍵字介紹

讓我們開始吧。我會使用一些在後面才會詳細說明的內容——在第一部分中請堅持一下。

非同步方法看上去就像這樣:

“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 非非同步方法的結果。

小貼士:如果你有一個非常簡單的非同步方法,你可能根本不會使用 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&lt;T&gt;,那麼它必須返回一個型別為 T 的值:

這裡有點兒奇怪,我們需要去適應。但這樣設計的背後,還是有一些“好的理由”的。

上下文

在概述中我提到過,如果你在等待一個內建的可等待操作,那麼可等待操作會捕捉當前的“上下文”,並在後面執行非同步方法剩餘部分時,使用該“上下文”。那麼“上下文”到底是什麼?

簡單的回答是:

  1. 如果你是在UI執行緒上,那麼它就是UI上下文。
  2. 如果你是在響應一個ASP.NET的請求,那麼它就是ASP.NET的請求上下文。
  3. 否則,通常它是一個執行緒池上下文。

複雜的回答:

  1. 如果 SynchronizationContext.Current 不為空,那麼它是當前的 SynchronizationContext(UI上下文和ASP.NET請求上下文都是 SynchronizationContext 上下文)。
  2. 否則,它是當前的 TaskScheduler(TaskScheduler.Default是執行緒池上下文)。

那麼在實際中這意味著什麼?首先,捕捉(以及儲存)UI、ASP.NET上下文是透明的。

對於事件處理來說,這是很棒的,但對於你要寫的其它大部分程式碼來說(實際上,這是你將會寫的大部分非同步程式碼),這可能並不是你想要的。

避免上下文

很多時候,你可能並不需要非同步回到“主”上下文。對於大部分非同步方法來說,在設計時會在頭腦中考慮組合:它們會等待其它操作,每個操作本身都代表一個非同步操作(這意味著,它們可以和其它操作組合在一起)。在這種情況下,你可以通過呼叫 ConfigureAwait 方法並傳入false的方式,來告訴“等待者”不要捕捉當前的上下文。例如:

在這個示例中, 有一點非常重要:每個“級別”上的非同步方法只會呼叫它自己的上下文。DownloadFileButton_Click 在 UI 上下文中啟動,然後呼叫 DownloadFileAsync 方法。DownloadFileAsync 也在 UI 上下文中啟動,但它通過呼叫 ConfigureAwait(false) 方法將上下文丟棄。DownloadFileAsync 方法剩餘部分會執行線上程池上下文中。然而,當 DownloadFileAsync 方法結束後,DownloadButton_Click 方法會恢復執行,它確實會恢復 UI 上下文。

一種好的做法:如果你不知道你確實需要上下文,那麼就用 ConfigureAwait(false) 方法。

非同步組合

到目前為止,我們只考慮了連續組合:非同步方法一次只會等待一個操作。我們也可以啟動多個操作,並等待其中一個(或者全部)結束。為此,我們可以啟動這些操作,但到後面再等待它們:

通過使用併發組合(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資料流的情況。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

Async 和 Await Async 和 Await

相關文章