Async

在多執行緒的森林裡,有些線,早已抽離了蠶的軀殼,它們不需繭的庇護,也能沿著記憶結點游走。我們誤以為是 await 編織了非同步的絲線,卻忘了真正編織的是那些未繭的任務。只要 Task 尚在,無論繭在與否,線都還在地底延展,繼續牽動著下一個狀態機的呼吸。


🎵 非同步的本質:回傳 Task 才是真的非同步

非同步的核心是 Task 或 Task 型別。只要方法回傳 Task,它就是非同步,因為 Task 代表著「進行中的工作」。
是否使用 await 不影響方法本身是否非同步,await 只決定要不要在這裡把結果拿回來、接著做後續的邏輯。

await 的本質是「把非同步操作的結果,用同步的方式 等待完成後,接著執行後續的程式碼」。


沒用 await

1
2
3
public Task<int> GetDataAsync() {
return GetDataFromDbAsync(); // 直接把 Task 回傳
}

這段程式:

GetDataFromDbAsync() 會回傳一個執行中的 Task。

呼叫端收到後可以選擇:先做其他事 → 等需要時再 await。

1
2
3
var t = GetDataAsync(); // t 是 Task<int>,執行中
// ... 可做其他事
var result = await t; // 在需要時等待結果


加 await → 只是把「等待」內嵌在方法內

1
2
3
public async Task<int> GetDataAsync() {
return await GetDataFromDbAsync();
}

差別在於:

  • 有 await 時,C# 編譯器會多生成一個狀態機。
  • 狀態機裡會記住「執行到哪裡停下來」與「完成後去哪裡繼續」。
  • 如果 Task 失敗了,例外也會從 await 處直接拋出。


🎵 什麼時候可以不加 await?什麼時候一定要?


【可以不加 await 的情況】

方法裡沒有後續邏輯要等 Task 結果處理,最後一行直接 return 執行後的 Task 就好,不需要多餘的狀態機。

  • Library 或中介層只把別人的 Task 包裝後丟出去,呼叫端自己決定要不要 await。
  • 組合場景 : 例如用 Task.WhenAll、Task.WhenAny 等。

👉 這樣做能 省掉狀態機生成,更高效,也不會多出執行緒上下文切換。



【一定要加 await 的情況】

但有兩個情境,一定要 await,否則執行結果永遠抓不到執行中的例外,只抓同步階段的

🔹 try/catch

1
2
3
4
5
try {
return await SomeAsync();
} catch (Exception ex) {
// 這裡可以抓到 SomeAsync 執行期間的例外
}

因為 await 才會讓例外回到目前執行緒,try/catch 才能接住!

🔹 using

1
2
3
4
using (var conn = new SqlConnection(...)) {
return await conn.DoSomethingAsync();
// 執行完才 Dispose()
}

馬上離開 using,可能還在用就被釋放!await 確保資源在非同步操作完成後才釋放。



🎵 狀態機的工作:記得「下一步要去哪裡」

再更深一層看:

await 會讓 C# 幫你生成 MoveNext 的狀態機方法。當 Task 尚未完成時,Compiler 會把當下執行上下文、狀態和要執行的後續操作,用 ContinueWith 掛在 Task 上。下次進來時,就知道該去哪個分支繼續跑。

  • 所以有後續邏輯要執行(像 DoJob3(result))→ 必須用 await。
  • 只是 return await … → 不需要,除非包在 try/catch 或 using。


🎵 參考文章

使用 .NET Async/Await 的常見錯誤
正確理解 C# async 與非同步



🎵 結語

async 與 await,只是蠶在枝頭短暫的吐絲與棲止;Task 才是那一縷未斷的生命,在你未察覺時,已悄悄生長成另一片網絡。記得,真正的非同步從不繫於語法,而是棲息在那條未繭的絲裡,等待你與它約定何時再度相逢。