.NET 非同步程式小技巧:GetAwaiter().GetResult() 與 Result 的差異
4 |
閱讀筆記 - 使用 .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,故遇到非同步函式需轉成同步執行,等待結果再繼續。在此測試四種不同寫法:
- RaiseErrorAsync().Wait()
- RaiseErrorAsync().Result
- RaiseErrorAsync().GetAwaiter().GetResult()
- 另類做法:將 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, 好文章,感謝分享,已加入本文。