去年整理過一篇研討會筆記 - 使用 .NET Async/Await 的常見錯誤,重新梳理 async /await 觀念,其中提到該用 .GetAwaiter().GetResult() 取代 .Result,以取得更明確的例外資訊,針對這點我還設計了實驗驗證 GetAwaiter().GetResult() 與 Result 的差異

昨天在臉書小朱分享一個不同觀點 - GetAwaiter() 是編譯器在用的,應用程式不要用。查了資料,發現這個說法來自官方文件

備註 這個方法是供編譯器使用,而不是用於應用程式程式碼。

由於中文文件來自機器翻譯,為避免失真,我參考了英文版,發現它的原文是:This method is intended for compiler use rather than for use in application code.,我解讀成「這個方法主要供編譯器呼叫,而非一般應用程式」,中文版翻譯並未違背原意。不過就程度上來說,我個人覺得比較像"一般應用程式不太會用到",還不到"不該用"或"不建議使用"的等級。

官方文件沒解說一般應用程式不會使用的理由,而依我的觀點,若程式都已用了 .Result,就沒理由不改用 GetAwaiter().GetResult(),至少遇到例外時會拿到實際 Exception,省去從 AggregateException.InnerException 挖掘真實錯誤訊息的麻煩,GetAwaiter() 的這點好處是公認的,但為什麼官方文件會說這方法主要是給編譯器用不是給一般應用程式?

研讀了一些文件,簡單整理如下。

首先,GetAwaiter() 編譯器真的用很凶 - 所有 await ... 語法都會被編譯器轉成 GetAwaiter()。(所以官方文件沒它主要是給編譯器使用,並沒有錯)

再來為什麼一般應用程式用不到?如果你的程式寫法全程 async/await,你不會用到 .Wait()、.Result,自然也就用不到 GetAwaiter(),對於身邊程式已全面 async 化的程式開發人員來說,的確沒什麼機會用到。我猜寫這份 API 官方文件的 RD 人員應該就屬於這類,因為擁有這樣的視角再合理也不過;至於還常跟老程式博鬥的古蹟維護小組人員,眼中看到的世界自然有點不一樣。

綜合以上兩點,我想這是為什麼官方文件說「GetAwaiter() 方法主要供編譯器呼叫,而非一般應用程式」的原因。

回到該不該用 .Wait()、.Result、GetAwaiter().GetResult() 議題上,首先要知道,這三者存在兩項共通風險 - Deadlock 跟 ThreadPool Starvation (執行緒短缺)。

.Result 混用 async 觸發 Deadlock 的案例之前我寫過文章,此處不贅述。至於 ThreadPool Starvation,起因於 .Wait()、.Result、.GetResult() 在等待過程將佔用 ThreadPool 中的 Thread,在高負載情境下會用光 ThreadPool 裡所有 Thread,但 ThreadPool 只會緩步增加數量(例如:增加到 1000 條需要數分鐘),若需求持續湧入便會供不應求,導致系統延遲效能不佳。

async/await 程式則能避免上述問題。使用 .NET Async/Await 的常見錯誤的第 4 點提到 async 程式會被編譯器轉成 IAsyncStateMachine 類別,await 前後被拆成兩段程式碼,透過 Callback 變更狀態機狀態銜接執行,與 .Wait()、.Result、.GetResult() 相比,可避免等待過程佔用 ThreadPool 資源。(延伸閱讀:.NET 編譯器對 C# 使用了 async / await 關鍵字程式碼,做了什麼事情 by Vulcan lee

好了,來下結論。

「該不該用 GetAwaiter()」是假議題,真正的關鍵提問是「你該不該用 .Wait()、.Result 或 GetAwaiter().GetResult()?」

正解是盡量不要,理由是有 Deadlock 及 Thread Starvation 風險,遇到需要它們的場合,請改用 async/await。BUT,每個選擇都有利弊,async 的傳染性眾所周知,一旦在方法前面加上 async 關鍵字,不得了, 裡面呼叫外部方法必須加上 await 才合規格,而要加 await 該外部方法順理成章也得加上 async,接著外部方法中又被要求使用 await... 就像病毒般四處蔓延,如果你有試過在既有複雜程式裡加上 async,肯定體驗過什麼叫「一發不可收拾」。因此,這又是抉擇問題,如果確信程式不會因為這些風險造成損失,用 GetAwaiter().GetResult() 快速實現非同步轉同步,有何不可?在決定系統架構、工具平台或程式寫法時,我這個人挺現實,認為所有決策都要基於利弊得失而非信仰(雖然有時不免要顧及公司或團隊信仰,但相信我,即使表面上像是為了信仰,利弊分析後你仍會找到客觀解釋,例如:支持 .NET 開發團隊用 C# 寫系統,而非強迫他們改學客戶偏好的 Java,是基於開發效率、軟體品質以及降低人員流動率考量),只要清楚自己在做什麼,評估過利弊得失,了解潛在風險,每個解法都可以是好解法。因此,對於「該不該用 GetAwaiter()/.GetResult()?」,我的答案是:

請優先考慮使用 async/await 寫非同步程式,它是較好的選擇;或者你不想搞懂那麼多細節,寫 async/await 也是首選,雖然麻煩些但不容易犯錯。但實務上就是有些情境用 GetAwaiter().GetResult() 比大量改寫 async 省時省力非常多,如果你已知道它有讓 Thread 卡住的副作用,也評估過 Deadlock 跟 Thread Stravation 風險,仍覺得值得,那麼... Just Do It! 至於 .Wait()、.Result,因為較不容易取得錯誤訊息,建議改寫成 GetAwaiter()。

【延伸閱讀】

MSDocs says "GetAwaiter() method is intended for compiler use rather than for use in application code.", this article tries to find "Why".


Comments

# by J

請問如果是必需要要用前人做好的 dll ,繼承某個 class 然後實作 method ,但是這些 method 都不是非同步的,也沒辦法改成 async/await ,那要在 method 中想要呼叫自己寫的非同步 method,最好的方式就是 GetAwaiter().GetResult() 嗎? 這樣看起來在舊有的同步方法想要呼叫非同步方法, Deadlock 跟 ThreadPool Starvation 問題是沒辦法避免的嗎? 懇請黑大解惑 謝謝

# by Jeffrey

to J, 如果不想或無法全面 async 化,一般就是靠 GetAwaiter().GetResult() 解決。Deadlock 是可以避免的,https://blog.darkthread.net/blog/await-task-block-deadlock/ 文章有一些說明,至於 ThreadPool Starvation,在高負載系統且效能要求較高時才會被突顯,沒那麼容易遇到。

# by J

感謝~

# by benstokes

System support specialists provide help desk assistance and technical support for all types of issues affecting end users https://www.fieldengineer.com/skills/system-support-specialist

Post a comment