這個標題的意思並不是要討論是否該寫非同步程式,而是要整理一個 FAQ:函式該直接回傳 Task 物件就好,還是一律使用 async/await?
寫法一:直接回傳 Task
物件。
public Task<int> DoSomethingAsync()
{
return CallDependencyAsync();
}
寫法二:即使是簡單的非同步呼叫也一律使用 async/await
。
public async Task<int> DoSomethingAsync()
{
return await CallDependencyAsync();
}
以上範例取自 David Fowler 撰寫的 Async Guidance 文件的其中一節:Prefer async/await over directly returning Task。
Fowler 的建議是採用寫法二,也就是盡量使用 async/await
,而不要直接回傳 Task
物件。他也在文中提到,直接回傳 Task 雖然能獲得稍微快一點的執行速度(因為它不用處理 async 狀態機的相關工作),但也失去了 async 狀態機帶來的一些好處,而且可能導致函式行為的改變。
兩種寫法的效能差異其實不大,通常不會是效能瓶頸之所在。故這裡推薦讀者採用 Fowler 的建議作法,也就是優先選擇採用 async/await 寫法來回傳非同步呼叫的結果。
如果你好奇直接回傳 Task
是否可能導致什麼比較嚴重的後果,以下範例展示了其中一種可能的狀況。
public Task<string> GetWebPageTask() { using var httpClient = new HttpClient(); return httpClient.GetStringAsync("https://www.microsoft.com"); }
上面的程式碼經過編譯之後會有一個try/finally
區塊,像這樣:
public Task<string> GetWebPageTask() { HttpClient httpClient = new HttpClient(); try { return httpClient.GetStringAsync("https://www.microsoft.com"); } finally { if (httpClient != null) { ((IDisposable)httpClient).Dispose(); } } }
由於 httpClient.GetStringAsync()
呼叫很可能尚未執行完畢,程式流程就進入了 finally
區塊而將 httpClient
物件回收,這將導致程式執行時發生 TaskCanceledException
。這或許是 David Fowler 在其文章裡面說這種寫法將造成程式的「行為改變」的原因之一。
👉 Try it on .NET Fiddle: https://dotnetfiddle.net/NRXmfr
採用 async/await 不只可以避免上述陷阱,還有其他好處。比如說,萬一非同步呼叫的過程發生錯誤,exception 物件的 stack trace 資訊會更完整詳細,能夠顯示真正發生錯誤的程式碼位置;相較之下,直接回傳 Task 的寫法,其 exception 的 stack trace 會不完整,可能不會提供正確的出錯位置。
有關 async/await 寫法的優點,在 Fowler 大大的原文裡面都有提到。完整起見,這裡用截圖的方式 highlight 出來:
結論
多數情況下,直接回傳 Task
的好處抵不過它帶來的問題,故建議在回傳非同步呼叫的結果時優先選擇 async/await 寫法。
Note: 有一種見解是,當函式呼叫層層套疊很多層的時候,便應該傾向直接回傳 Task 物件。但我想還是應該基於是否真的足以產生「有實質意義上的效能差異」來決定,而不是有比較快就好。而且,直接回傳 Task 物件還有一些潛在的問題和缺點(如前面提過的),可能增加日後維護程式的麻煩,最好也納入考量。
See also
- Async Guidance by David Fowler
- Async/Await - Best Practices in Asynchronous Programming by Stephen Cleary
沒有留言: