前文提過一個撰寫非同步程式的通用建議:從頭到尾都採用非同步呼叫。可是,有時候就是沒辦法做到這點。
比如說,當我們想要在 console 應用程式的 Main 方法當中呼叫非同步方法,像底下這樣寫,是無法通過編譯的:
讀過前面的內容,現在你應該很清楚第 4 行無法通過編譯的原因了:只要函式裡面有用到
好,那就試試加上
這樣會變成宣告
在實際開發應用程式時,可能也會碰到類似情形,亦即底層函式庫提供的是
改成以下的寫法則沒有問題:
這裡採用的解決辦法是把非同步呼叫的部分包在另一個
現在考慮相反的情況:假設你正要使用一個現成的函式庫,那個函式庫沒有提供非同步版本的 API。於是,為了讓自己寫程式的時候可以從頭到尾都採用非同步呼叫,你打算另外寫一個
請注意這裡使用了
小結
如果沒辦法撰寫「真正的」非同步方法,最好別假裝它是。這樣的話,至少別人在使用你的函式庫時,一眼就能判斷那是個同步方法,不至於因為誤用而導致捉摸不定的效能問題。
參考資料
比如說,當我們想要在 console 應用程式的 Main 方法當中呼叫非同步方法,像底下這樣寫,是無法通過編譯的:
static void Main() { var client = new HttpClient(); string content = await client.GetStringAsync("http://www.google.com"); // 編譯失敗! }
讀過前面的內容,現在你應該很清楚第 4 行無法通過編譯的原因了:只要函式裡面有用到
await
,則該函式在宣告時必須加上 async
關鍵字。好,那就試試加上
async
:static async void Main() { // 略 }
這樣會變成宣告
Main
方法的那行無法通過編譯,因為 console 應用程式的進入點不能宣告為 async
方法。在實際開發應用程式時,可能也會碰到類似情形,亦即底層函式庫提供的是
async
方法,可是呼叫端本身無法宣告成 async
方法。改成以下的寫法則沒有問題:
static void Main() { var t = MyDownloadPageAsync("http://www.google.com"); t.Wait(); } static async Task MyDownloadPageAsync(string url) { var client = new HttpClient(); string content = await client.GetStringAsync(url); Console.WriteLine(content.Length); }
這裡採用的解決辦法是把非同步呼叫的部分包在另一個
async
方法裡面,也就是 MyDownloadPageAsync
。然後,在呼叫端接收該方法所返回的 Task
物件,並呼叫它的 Wait
方法來等待非同步工作執行完畢。這是在不得已的情況下使用 Task
的 Wait
方法。現在考慮相反的情況:假設你正要使用一個現成的函式庫,那個函式庫沒有提供非同步版本的 API。於是,為了讓自己寫程式的時候可以從頭到尾都採用非同步呼叫,你打算另外寫一個
async
方法來包裝那個同步呼叫的 API。也許會像這樣:public async Task MyDownloadPageFakedAsync(string url) { var client = new WebClient(); var task = Task.Run(() => { string content = client.DownloadString(url); Console.WriteLine("網頁長度: " + content.Length); }); await task; }
註:我們當然知道使用此函式的人,很可能無法看到函式內部的實作,所以光從函式的宣告來看:有WebClient
有提供DownloadString
的非同步版本,這裡只是為了方便說明而刻意使用同步呼叫的版本。
async
關鍵字、返回 Task
物件,而且函式名稱以 “Async” 結尾,自然會認為那是個非同步方法。既然是非同步方法,那就不見得會動用執行緒。結果,卻完全不是那麼回事。怎麼說呢?請注意這裡使用了
Task.Run()
方法來建立一個非同步工作,以便在此函式中使用 await
,以及在宣告時加上 async
。換句話說,這裡使用了 Task.Run()
來把同步執行的工作偽裝成非同步方法。然而,當你使用 Task
類別來建立非同步工作時,預設的工作排程器會向執行緒集區(thread pool)調動一個工作執行緒來執行任務。如此一來,使用這個函式的人會以為它跟其他 async
函式一樣,卻不知道它背後其實使用了執行緒集區——如果大量用於 ASP.NET 應用程式中,可能導致效能或者延展性(scalability)不佳的問題(因為跟 ASP.NET 爭搶使用執行緒集區可能會導致集區耗盡)。小結
如果沒辦法撰寫「真正的」非同步方法,最好別假裝它是。這樣的話,至少別人在使用你的函式庫時,一眼就能判斷那是個同步方法,不至於因為誤用而導致捉摸不定的效能問題。
參考資料
- Channel 9 短片:Tip 4: Async Library Methods Shouldn't Lie
沒有留言: