textMessage

她在深夜寫了一段訊息,對著那個熟悉的對話框,輕輕按下「傳送」

網路似乎是卡住了,畫面顯示「正在傳送…」,她關上了手機,心想:「應該已經送出了吧。」

隔天,她等不到回應。那人說沒收到。

她打開訊息記錄,那串話仍顯示為「未送達」,就像訊息從未存在過

一個沒有被接收的訊息,不會自動變成世界的理解;一個沒有被等待的結果,也無法保證會如你所想地完成

f

在實務開發中,我們經常面臨一種情境:必須等待某個非同步任務完成,才能繼續執行接下來的邏輯。但如果我們當下的環境是同步方法(例如 Main() 或某些事件處理函式),該怎麼辦?

f

這時,許多開發者會選擇以下方式強行「同步化」等待

  • .Wait()
  • .Result
  • GetAwaiter().GetResult()

它們都會強迫等待 Task 結束,但錯誤處理機制卻有天壤之別,而這個差異,正是許多初學者第一次遇到的陷阱。
以下,我們將模仿實驗,實際觀察其錯誤行為與影響,像是 .Wait().Result 不是把錯誤丟給你,而是「包一層再丟」,讓你更難看清真相

f

實驗出處

你有一個 async 方法

1
2
3
4
5
6
7
8
9
10
11
static async Task<string> SumulateDatabaseConnectionAsync()
{
Thread.Sleep(2000);
if (DateTime.Now > new DateTime(2020, 12, 12))
{
throw new InvalidDataException("db 連壞掉啦!!");
}

return "db 連到啦";
}

你身處同步世界

1
static void Main()

我們用同一個會「一定丟錯」的 async 方法,換三種同步等待方式,看 catch 到的是什麼鬼東西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static void Main()
{
TestSip("Wait", () => {
SumulateDatabaseConnectionAsync().Wait();
});

TestSip("Result", () =>
{
var result = SumulateDatabaseConnectionAsync().Result;
});

TestSip("使用 GetAwaiter().GetResult()", () =>
{
var result = SumulateDatabaseConnectionAsync().GetAwaiter().GetResult();
});


TestSip("Fire And Forget", async () => {
await SumulateDatabaseConnectionAsync();
});

WriteColorLine("主程序結束!", ConsoleColor.Cyan);
}

static void TestSip(string testName, Action callback)
{
WriteColorLine("=========================================", ConsoleColor.Yellow);
WriteColorLine($"測試 : {testName}", ConsoleColor.Green);
WriteColorLine($"開始時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Gray);

try
{
callback();
WriteColorLine($"結束時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Gray);
WriteColorLine("操作成功完成", ConsoleColor.Green);
}
catch (Exception ex)
{
WriteColorLine($"結束時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Red);
WriteColorLine($"錯誤 : {ex.Message}", ConsoleColor.Red);
if (ex.InnerException != null)
{
WriteColorLine($"內部錯誤 : {ex.InnerException.Message}", ConsoleColor.Red);
}
}
}

static void WriteColorLine(string message, ConsoleColor colorName)
{
Console.ForegroundColor = colorName;
Console.WriteLine(message);
Console.ResetColor();
}

結果分析

.Wait() / .Result

.Wait().Result 是 「同步阻塞 async Task」 的做法,在 .NET 設計上一個 Task 可能同時有多個例外,所以它一定用 AggregateException 當外殼,它不是把錯誤丟給你,而是先幫你包一個你沒要的殼再丟。.Wait() / .Result 會把真正的例外包進 AggregateException,當我們 catch (Exception ex) 時 ex.Message 是 假的,真正錯誤藏在 InnerException

f

GetAwaiter().GetResult()

它模擬的就是 await 的行為,只是少了語法糖,async 裡丟什麼,你就接到什麼,而 stack trace 也比較乾淨

f

Fire-and-Forget(async lambda)

你以為你在等,其實你只是假裝沒看到錯誤。TestSip 接的是 Action,async lambda 會變成 async void,例外不會回到 TestSip、也 catch 不到,直接交給 runtime

f

f

f

f