閱讀筆記 - 使用 .NET Async/Await 的常見錯誤提到「需要等待結果才繼續執行的場合,宜用 .GetAwaiter().GetResult() 取代 .Result」,這點喚起我過去寫非同步程式的回憶:設了 try catch 也捕捉到錯誤,但回拋的錯誤訊息看不出所然。

既然學會 GetAwaiter().GetResult() 技巧,就來個實地驗證,加強記憶。

我寫了一個測試程式:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Test("Wait()", () =>
            {
                RaiseErrorAsync().Wait();
            });
            Test("Result", () =>
            {
                var s = RaiseErrorAsync().Result;
            });
            Test("GetAwaiter().GetResult()", () =>
            {
                var s = RaiseErrorAsync().GetAwaiter().GetResult();
            });
            Test("Fire and Forget", async () =>
            {
                var s = await RaiseErrorAsync();
            });
            Console.WriteLine("Done!");
        }

        static void Test(string testName, Action callback)
        {
            Console.WriteLine("=================================");
            Console.WriteLine($"Test {testName}");
            Console.WriteLine($"Start {DateTime.Now:mm:ss}");
            try
            {
                callback();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Stop {DateTime.Now:mm:ss}");
                Console.WriteLine("Error: " + ex.Message);
                if (ex.InnerException != null)
                    Console.WriteLine("Inner Error: " + ex.InnerException.Message);
            }
        }

        static async Task<string> RaiseErrorAsync()
        {
            Thread.Sleep(2000);
            if (DateTime.Now.CompareTo(new DateTime(2012,12,21)) > 0) 
                throw new ApplicationException("刻意產生錯誤");
            return DateTime.Now.ToString();
        }
    }
}

Test() 方法接受呼叫端傳入 Action 型別的 callback 參數,方法內部用 try catch 包住 callback(),在出錯時印出錯誤。 要測試的 async 函式為 RaiseErrorAsync(),原本應傳回 DateTime.Now 字串,但為觀察錯誤處理情形及等待效果, 我刻意讓它先 Thread.Sleep(2000) 延遲兩秒,再丟出 ApplicationException。

Test() 非 async 函式不能使用 await,故遇到非同步函式需轉成同步執行,等待結果再繼續。在此測試四種不同寫法:

  1. RaiseErrorAsync().Wait()
  2. RaiseErrorAsync().Result
  3. RaiseErrorAsync().GetAwaiter().GetResult()
  4. 另類做法:將 callback 宣告成 async,以便在其中 await RaiseErorrAsync()。 但 Test() 非 async,呼叫時不能 await,故直接呼叫 callback 將不等待結果繼續往下執行,相當於 Fire and Forget。 這是不建議的做法,等下我們也會看出它的缺點。

實測結果如下:

.Wait()、.Result 跟 .GetAwaiter().GetResult() 都有讓 async 函式同步化的效果,程式會等待 RaiseErrorAsync() 執行再繼續, 故輸出結果可看出 Start 與 Stop 有兩秒差距。但 .Wait() 與 .Result 時 catch 到的是 AggregateException, 故 ex.Message 為 One or more errors occurred; 如上回文章所說,改用 .GetAwaiter().GetResult() 可以改善這個問題,一樣會等待結果,但錯誤訊息來自 ApplicationException, 不像 AggregateException 需再鑽一層從 InnerException 取得真實錯誤原因。 遇到 UI 直接回拋 ex.Message 的場合,偵錯資訊會明確許多。

值得留意的是,第 4 種 Fire and Forget 測試時,只印出了 Start,沒出現錯誤程式就結束了。

修改一下程式,印出 Done! 之後我們再等 5 秒。

//...以上省略...
            Console.WriteLine("Done!");
            Thread.Sleep(5000);
        }

實測結果如下。多等一下,錯誤就會出現了。但因為射後不理,Test() 呼叫 callback 後就跳出 try catch 範圍,callback 發生的錯誤成為未被處理的例外導致程式中斷。 另一件有趣的事是第四種 callback 用了 await RaiseErrorAsync(),錯誤訊息的 StackTrace 出現 TaskAwaiter`1.GetResult(),代表 await 背後也是用 GetResult(); 另外 StackTrace 也出現了上次文章提到 async 函式會被轉成 IAsyncStateMachine 類別後內含 switch case 的 MoveNext() 方法。

以上突顯了 Fire and Forget 的風險:async 函式拋出的錯誤可能被無視,或跳脫 try catch 範例成為未被處理的例外導致程式中斷。

不花一毛錢,隨時隨地可以自己做實驗印證理論,這也是寫程式的迷人之處! 😛

[2019-08-08更新] 感謝讀者 Mark 補充 MSDN Magzine 的好文章:Async/Await - Best Practices in Asynchronous Programming,對於 async void 使用時機有很詳細的說明,值得一讀。

This article uses a experiment to demostrate the difference between GetAwaiter().GetResult() and .Result.


Comments

# by Ike

[Typo] 但 .Wait() 與 .Result 時 catch 到的是 AggrateException => AggregateException

# by Jeffrey

to lke, 難怪看起來怪怪的 XD 謝謝指正。

# by Mark

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx Async/Await - Best Practices in Asynchronous Programming 這篇有明確告知甚麼能做,甚麼不能做,無論如何都要怎麼做。

# by Jeffrey

to Mark, 好文章,感謝分享,已加入本文。

Post a comment