Asynchronous Programming - 第十一章:未繭之絲:任務才是本體
在多執行緒的森林裡,有些線,早已抽離了蠶的軀殼,它們不需繭的庇護,也能沿著記憶結點游走。我們誤以為是 await 編織了非同步的絲線,卻忘了真正編織的是那些未繭的任務。只要 Task 尚在,無論繭在與否,線都還在地底延展,繼續牽動著下一個狀態機的呼吸。
🎵 非同步的本質:回傳 Task 才是真的非同步
非同步的核心是 Task 或 Task
是否使用 await 不影響方法本身是否非同步,await 只決定要不要在這裡把結果拿回來、接著做後續的邏輯。
await 的本質是「把非同步操作的結果,用同步的方式 等待完成後,接著執行後續的程式碼」。
沒用 await
1 | public Task<int> GetDataAsync() { |
這段程式:
GetDataFromDbAsync() 會回傳一個執行中的 Task。
呼叫端收到後可以選擇:先做其他事 → 等需要時再 await。
1 | var t = GetDataAsync(); // t 是 Task<int>,執行中 |
加 await → 只是把「等待」內嵌在方法內
1 | public async Task<int> GetDataAsync() { |
差別在於:
- 有 await 時,C# 編譯器會多生成一個狀態機。
- 狀態機裡會記住「執行到哪裡停下來」與「完成後去哪裡繼續」。
- 如果 Task 失敗了,例外也會從 await 處直接拋出。
🎵 什麼時候可以不加 await?什麼時候一定要?
【可以不加 await 的情況】
方法裡沒有後續邏輯要等 Task 結果處理,最後一行直接 return 執行後的 Task 就好,不需要多餘的狀態機。
- Library 或中介層只把別人的 Task 包裝後丟出去,呼叫端自己決定要不要 await。
- 組合場景 : 例如用 Task.WhenAll、Task.WhenAny 等。
👉 這樣做能 省掉狀態機生成,更高效,也不會多出執行緒上下文切換。
【一定要加 await 的情況】
但有兩個情境,一定要 await,否則執行結果永遠抓不到執行中的例外,只抓同步階段的
🔹 try/catch
1 | try { |
因為 await 才會讓例外回到目前執行緒,try/catch 才能接住!
🔹 using
1 | using (var conn = new SqlConnection(...)) { |
馬上離開 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 才是那一縷未斷的生命,在你未察覺時,已悄悄生長成另一片網絡。記得,真正的非同步從不繫於語法,而是棲息在那條未繭的絲裡,等待你與它約定何時再度相逢。