在即將到來的新的Windows Runtime中更根本地確定任何API都不會運行超過50ms的時間。需要更長時間的操作將會由'kick off this operation'API來代替,不等待運算結果就直接立刻返回。這樣做是因為Microsoft希望Windows8 Metro程序能夠在即時的觸控UI上能夠“快速並且流動”,因為觸控操作上即使是微小的停頓相比於用鼠標或者鍵盤來操作都會變得更加明顯。從UI的角度來說,這是一項很有幫助的設計方案。
但是從開發者的角度來說,它會使編程變得更加麻煩。當我們讀取文件或者調用WCF服務時,我們通常希望能夠影響到結果。如果能夠保證讀取文件或者WCF服務返回時結果肯定可獲得的,我們由上而下地寫出容易理解和推理的代碼。
string url = ReadUrlFromFile(filename);
string contentOfUrl = HttpGetFromUrl(url);
MessageBox.Show(contentOfUrl);
這樣的API被叫做同步或者阻塞。同步API易於理解和使用,不過在你的程序內部當前線程沒有反應時,API就無法控制你的代碼去做其它任務,因為它還不能傳遞結果。
擁使用即時返回的'kick off' API的方式叫做異步或者無阻塞。使用異步API編程更加繁難,因為你不能即時將結果返回給變量來保證運行:
string url = BeginReadUrlFromFile(filename); // Won't work -- file read hasn't completed when BeginRead returns
string contentOfUrl = BeginHttpGetFromUrl(url); // Ditto
MessageBox.Show(contentOfUrl);
相反,你不得不回調需要使用返回結果的代碼,直到它已經准備好了:
BeginReadUrlFromFile(filename, url => {
BeginHttpGetFromUrl(url, contentOfUrl => {
MessageBox.Show(contentOfUrl);
});
});
甚至於這樣一個簡單的例子都顯得相當丑陋。實際上,異步代碼中需要更多的運算,更復雜的回調,邏輯條件,early exits以及錯誤處理,所以會更加難看。.NET框架中真正的異步API更加丑陋,到處都是 IAsyncResult對象和成對的EndXxx方法調用。
然而,如果我們希望能程序在Windows Runtime中運行,這就是用戶所希望我們做的。
原來的解決方案:使用F#
F#背後聰明的人們想出來一個兩全其美的解決方案。F#有一個叫做異步工作流程的特色功能,它由很多塊異步引進的代碼塊組成。在異步工作流程中,你可以通過使用一個很像同步的語法來調用異步方法:
async {
let! url = BeginReadUrlFromFile filename
let! contentOfUrl = BeginHttpGetFromUrl url
MessageBox.Show(contentOfUrl)
}
F#編譯器自動將這易於閱讀、理解的代碼轉變為可怕的回調式等價物,這樣你就可以簡單地使用異步調用的響應行為從上而下地編程。
新的解決方案:使用C# 5
C#背後也有同樣聰明的人,所以新的C#中也實現了這項功能。Visual Studio 11 beta中包含的下一版本的C#進了兩個新關鍵字——"async" 和 "await"
關鍵字"async"表明使用的是異步調用方法。這對於調用者來說,理解它非常重要,因為這意味著方法會在它結束前返回——方法能夠在異步調用時中途放棄而直接返回給它的調用者。
關鍵字"await"表明我們希望保證自上而下的邏輯 異步調用 而不是手動編寫回調函數。下面是他們完美結合在一起的例子:
public async void ShowReferencedContent(string filename) {
string url = await BeginReadFromFile(filename);
string contentOfUrl = await BeginHttpGetFromUrl(url);
MessageBox.Show(contentOfUrl);
}
這樣比回調更方便讀、寫和檢查,但他們的作用完全相同。(實際上,這確實比回調更智能些,因為編譯器並不會因為厭煩而跳過錯誤狀況或者弄錯early exit邏輯又或者忽略線程錯誤。)
當我們調用方法時發生了什麼?首先是調用BeginReadFromFile方法,它提供了文件名和編譯器生成的回調。BeginReadFromFile迅速返回,但結果仍然不可得。所以結果仍然不能分配給URL變量——回調的一部分——然後方法退出,返回給調用者!調用函數重新運行,並且保持它的代碼持續運行,盡管被調用方法還沒有結束。
然後在晚點時候,文件系統完成了閱讀操作。這意味著結果現在是可獲得的,Runtime安排回調。這並不一定會立刻發生——具體的時間還依賴於同步的環境。回調函數運行著,捆將URL變量和文件操作的結果綁定,然後調用BeginHttpGetFromUrl。它也會立刻返回,也就是說,方法會再一次退出。
最後,HTTP操作完成,回調函數第二次運行。它將綁定Url變量的內容和顯示結果的消息框如果(如果有的話)。
我會希望向調用者返回什麼值?
Async methods can exit before they’ve finished. So if an async method wants to return a result, it has to recognise that it might return to the caller before that result is available. For this reason, an async method that returns a value has to have a return type of Task rather than a ‘proper’ value. A Task represents a chunk of work which will eventually deliver a value, so a caller can examine the returned Task to determine when the result becomes available. Here’s how an async method looks when returning a value:
異步方法能夠在結束前退出,所以,如果一個異步方法希望返回一個結果就不得不確認它是否在得到結果前就返回給調用者。因此,一個返回值的異步方法不得不包含一個Task返回類型而不是一個“合適的”值。一個Task代表最終會傳遞值的很大一塊工作,所以調用者也能堅持返回的Task來確定什麼時候會得到結果。下面是一個返回值的異步方法的樣子:
public static async Task<string> GetReferencedContent(string filename)
{
string url = await BeginReadFromFile(filename);
string contentOfUrl = await BeginHttpGetFromUrl(url);
return contentOfUrl;
}
注意:盡管返回類型是Task<string>,返回狀態接收的是一條字符串。再一次,編譯器來管理返回狀態產生一個Task。
現在調用者能夠直接調用GetReferencedContent方法或者等待字符串變為可得,或者手動讓它等待,又或者使它提前結束——無論如何它都適合使用結果。
Async-friendly APIs
如果你習慣在.NET 4或者更早之前版本上使用異步編程,你會習慣成對地使用Begin和End方法,比如WebRequest.BeginGetResponse 和WebRequest.EndGetResponse。這在.NET4.5中依然存在,但它們不使用await關鍵字。(主要是因為BeginXxx方法需要在回調中使用確切的方法調用來得到結果,而且編譯器並不依賴EndXxx命名規范).NET4.5提供了返回Task對象的新方法,所以你可以調用WebRequest.GetResponseAsync來代替WebRequest.BeginGetResponse方法。下面是一個.NET4.5中使用異步API的一個實例:
private static async Task<string> GetContent(string url)
{
WebRequest wr = WebRequest.Create(url);
var response = await wr.GetResponseAsync();
using (var stm = response.GetResponseStream())
{
using (var reader = new StreamReader(stm))
{
var content = await reader.ReadToEndAsync();
return content;
}
}
}
這和使用 WebRequest.GetResponse() 和 TextReader.ReadToEnd()的同步代碼是如此相似,只需要在API名後加上Async並且在方法前加上"await"關鍵字就可以了,相信你很快就能掌握它。