Asynchronous - 第一章:雲端中的未竟之事

咖啡廳裡,播放著熟悉的爵士樂,氣味是熱牛奶與咖啡豆交融後的溫暖。呼叫器還沒響,但我確定店員剛剛有聽見我點了那杯厚拿鐵
我不確定她現在是不是正在打奶泡,或是還在處理上一張訂單;但我不需起身確認,我選擇坐著,等待震動響起地那一刻
🎵 段落一:傳訊以後的沉默
這是一段將備份資料上傳到 S3 的程式碼
1 | private async Task UpdateS3DataAsync(string s3RecordData, string s3Path) |
測試了幾次,未能在 S3 Bucket 見到資料的身影,程式也沒有拋出任何錯誤,程式碼像沒有執行過一樣
然而問題不在程式碼本身,而在呼叫端的態度,沒有 await、或 GetAwaiter(),甚至連 Task.Result 都沒取,就這樣任由任務派發出去而不管結果如何
1 | // 呼叫端完全沒有等待結果, 也沒有取得任務執行的狀態 |
因為呼叫端忽略了下達 await,導致非同步邏輯變成了放生的任務,它在背景默默執行,但主流程不關心它是否完成、是否失敗,就像你傳訊息出去但不管對方是否收到一樣
而程式碼修正的方式可分成兩種…
方式一:明確等待,當下確保得到任務執行結果才往下執行
1 |
|
方式二:先執行、後等待,彈性處理任務進度
1 |
|
這種寫法帶給我們更多自由,你可以先發出請求、做點別的事情,等所有任務都在執行的路上,再一起收尾。適合需要進行多工處理或並行任務的場景
🎵 段落二 : 將方法標記為 async,呼叫端 await 的意義
當我們在定義的方法加上 async 修飾詞,就會啟用下面兩項非常關鍵的功能
功能一 : 標記為 async 的方法內可以使用 await。而這個 await 就像是故事的中場休息,它告訴編譯器:「這裡先不要急著往下走,等我拿到結果再繼續。」,與此同時,控制權會被交還給呼叫端,讓主流程可以決定繼續跑別的事情
功能二 : 被 async 標記的方法,會自動回傳一個 Task(或 Task
)給呼叫端,也就是說呼叫端可以拿到任務執行完成與否
整個非同步的生命週期如下
當程式執行到 await ,它會先「記住目前的位置」(也就是等下回來繼續的地方),然後把控制權交還給呼叫端,讓其他任務可以先繼續跑。此時,非同步任務會在背景悄悄啟動(例如發出 API、寫檔、上傳 S3 等等)。等到背景任務真的完成時,程式會「回到剛剛記住的地方」,繼續往下執行剩下的程式碼。
- async:讓方法能「中途暫停」、「回傳 Task」
- await:告訴方法「請等它完成我才要往下做」
- Task:是一張「未完成的回應紙條」,你可以隨時等它完成,或是暫時放著做別的事
🎵 段落三 : 關於 Task 的實驗
- 我們紀錄起始時間,再呼叫 LogAsync(),但不立刻等它完成,而是先繼續往下執行並記錄時間。
- 最後才透過 await task,等待這個工作真正結束印出結果
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
27async void Main()
{
var startTime = DateTime.Now.ToString("HH:mm:ss");
Task task = LogAsync();
var endTime = DateTime.Now.ToString("HH:mm:ss");
$"{startTime} -- {endTime}".Dump();
await task;
$"完成沒 : {task.IsCompleted}".Dump();
}
public async Task LogAsync()
{
await Task.Delay(5000);
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "log.txt");
using (var writer = new StreamWriter(path, true))
{
await writer.WriteLineAsync(DateTime.Now.ToString("HH:mm:ss"));
}
}
// 22:22:31 -- 22:22:31
// 完成沒 : True
主線程不會被卡住,它會說:「我先記下這個任務,等你回來我再處理你回報的結果。」
🎵 段落四 : 用洗內褲的故事來說明非同步…
1 | void Main() |
想像你是執行緒本尊,一條誠懇又努力的 Main Thread。你每天負責處理生活中各種重要邏輯,但偶爾也要負責洗內褲(尺寸 XXXXXXXL、滿是泥巴、極度資源密集),這種事如果你親自處理,會讓主要的生活邏輯原地卡死。但如果你能派出一個任務非同步的去處理這件事( new Task(() => …)),就能在等待的這段時間裡,去追劇、去 dump log、去感動、去活著
🎵 樂曲回顧 : 關於 Throughput
學非同步時可能會想問:「這樣程式會跑得比較快嗎?」
而事實上,非同步的核心價值不是讓單一任務變快,而是讓 App 在同一段時間內完成更多任務,這是所謂的產能(Throughput)的提升,而不是效能(Performance)的進步
我們用工廠來想像一下:
假設一位工廠員工在製作產品 A。製作途中有個「焊接」步驟需要請外包來完成。這時,他把產品 A 交給外包,然後立刻開始製作產品 B,而不是站在原地等 A 回來。等外包完成後,把產品 A 送回工廠,這時再由另一位員工將它包裝完成。這樣的流程中,產品 A、B 各自都花了一樣的製作時間,但因為我們不浪費等待的空檔,整體工廠在同一段時間內完成了更多產品,就是非同步帶來的「Throughput 增加」
而非同步最適合處理「I/O Bound 工作」,也就是那些大部分時間都在等別人回覆的事情
- 從硬碟讀寫檔案
- 查資料庫
- 呼叫外部 API
- 與外部設備、服務通訊
這些操作都會讓 Thread 處於「等人回信」的狀態。如果你用同步方式處理,就會浪費一個 Thread 傻傻等著。但如果操作非同步,就能在等待期間釋放該 Thread 去做別的事,等到回信來了再接著處理剩下的工作
而今天若是 CPU Bound 的工作就不同了,比如要同時運算一堆複雜公式、解壓縮大量圖片或跑深度學習模型,這些都是「全程都在燒腦」的任務。這時 Thread 根本沒有閒著,也就沒有什麼好「釋放」的空間了,非同步的優勢自然就會變得不明顯
🎵 結語:那杯咖啡,終究會響起提示音
你知道咖啡師正在忙、訂單正在準備,你也知道,等到一切都準備好,那杯屬於你的咖啡,終究會震動提醒你「可以來取了」
非同步程式設計教會我們的,是一種更成熟的節奏感:不要卡在原地,不要什麼事都全程參與,需要學會把任務交出去,讓資源被更有效地運用,並且在正確的時候回頭、收尾

