Asynchronous Programming - 第十章:未完成的回聲
在每一個任務被派遣出去的夜裡,總有一些聲音,無法即刻歸來。它們在平行的執行緒裡交錯穿行,有的抵達了終點,返回一聲輕快的完成;有的在等待中腐蝕,成為無聲的阻塞;有的半途拋下誓言,消失在取消的荒野裡。你以為程式碼寫好了,流向已然明朗,卻不知每個 Task 都像流浪的信件,封存著可能,也埋藏著未竟。
TPL,不只是一套平行的工具,它是一場關於 等待與命運 的實驗,每一個 WaitAll、WhenAny、Result,都是開啟分支與結局的咒語。當結果未歸,回聲仍在。在執行緒的深處,有些任務,終將完成;有些回聲,永遠未完。
🎵 簡單的建立一個 Task
先用最簡單的 Task.Run 建立一個任務,看看它跟一般程式碼的執行順序有什麼不一樣。
1 |
|
✅ 執行結果
1 | Main Thread!!!!! |
為什麼只印出 Main Thread!!!!!?
這是因為 Task 預設是 非同步(Asynchronous) 執行的,
意思是:
你叫他去做事(開背景工作)後,主程式不會停下來等他做完。
主程式執行緒繼續往下跑,直接把 Main Thread!!!!! 印出來。
背景執行的 Task 需要一點時間(Thread.Sleep(1000)),結果還沒來得及印,主程式就跑完關閉了。
🎵 那要怎麼「等他」?
可以在 Main 裡多加一個 Console.Read() 或 Console.ReadLine(),
讓主執行緒在結束前卡住,等你看完輸出。
1 |
|
✅ 執行結果
1 | Main Thread!!!!! |
因為多了 Console.Read(),程式不會馬上結束,所以等到背景任務跑完後,也會看到 Try Task!!!!!!!!。
🎵 兩個任務 + 等待
現在我們試試看同樣的概念,開兩個 Task,然後確保兩個都做完後再繼續執行主流程。
1 |
|
看到上一個例子,我們知道 Task 開始執行我們就不會停在那邊等他完成,會繼續跑主流程,因此這邊我們用 WaitAll 阻塞,預期上,直到 task1、task2 完成前,Main Thread 不會冒出來
✅ 執行結果
1 | Task 1 完成溜 |
當你同時啟動多個 Task 時,主執行緒一樣不會等它們。Task.WaitAll 是一個「同步阻塞(Blocking)」的方法,會卡在那裡,直到你列出的所有 Task 都執行完才往下跑。
你派兩個小朋友去倒垃圾和買飲料,但你要等他們都回來才能關門去睡覺。
🎵 等待結果回傳再繼續跑
1 |
|
✅ 執行結果
1 | Done!! |
🗝️ .Result 的本質
taskResult 是一個 Task,它裡面包了一個「還沒做好的結果」。當你呼叫 .Result 時,程式會 阻塞(Blocking):
如果結果還沒算好,就會在那裡等。等到結果算好了,就把值回傳給你。
🎵 觀察 Task 的執行狀態
實驗目標:觀察 Task 的執行狀態
這段程式想要測試幾件事:
- Task.WhenAny 怎麼做多任務競賽(誰先完成)。
- CancellationToken 怎麼拋出取消例外。
- try/catch/finally 怎麼正確分辨 Completed、Canceled、Faulted 三種狀態。
- 怎麼把同步輸入(Console.ReadKey)包成 Task 跟 Delay 比賽。
程式執行流程
1️⃣ 先準備好取消功能
1 | var cts = new CancellationTokenSource(); |
2️⃣ 開一個 Task,裡面邊等輸入邊計時
用 Console.ReadKey() 讓使用者按 A/B/C 其中一個按鍵。每秒檢查一次,用 Task.WhenAny 誰先完成就執行誰(誰先:使用者輸入 or 時間到)。
3️⃣ 如果輸入了,就依輸入執行不同分支
- A:正常完成,回傳 “OK”
- B:拋例外,模擬異常狀況
- C:呼叫取消,丟出 OperationCanceledException
- 其他:回傳 Unknown
4️⃣ 5 秒都沒輸入就回傳 “No Input!”
這時 Task 也算是正常完成。
5️⃣ 主流程 try/catch/finally 分別處理三種可能結果
- OperationCanceledException → Canceled
- 其他 Exception → Faulted
- 正常 → Completed
實作
1 |
|
✅ 執行結果與分析
🔵 1. 什麼都不輸入 → 正常結束
1 | Please Choose a Status : A : Completed, B : Fault : C : Cancel |
Console.ReadKey() 沒有輸入,delayTask 每次都先完成。5 次迴圈後,直接回傳 “No Input!”,代表任務是正常完成的,狀態是 Completed。
🟢 2. 5 秒內輸入 A → 正常完成
1 | Please Choose a Status : A : Completed, B : Fault : C : Cancel |
Console.ReadKey() 輸入 A 後,inputTask 先完成。Task 直接走到 return “OK”。狀態是 Completed。
🔴 3. 5 秒內輸入 B → 發生例外
1 | Please Choose a Status : A : Completed, B : Fault : C : Cancel |
B 會執行 throw new ApplicationException。這會讓 Task 變成 Faulted 狀態(裡面有 Exception)。進入 catch (Exception) 區塊,印出錯誤訊息。
🟡 4. 5 秒內輸入 C → 取消
1 | Please Choose a Status : A : Completed, B : Fault : C : Cancel |
輸入 C 時,呼叫 cts.Cancel() 並且 ThrowIfCancellationRequested()。這會拋出 OperationCanceledException。Task 變成 Canceled 狀態。進入 catch (OperationCanceledException)。
🎵 筆記來源
🎵 結語
每一個被派遣出去的 Task,就像一隻小獸,被放進平行的森林裡,去完成牠們各自的使命。
有些小獸輕巧,踏過阻塞的河流,攜著結果回來,在主執行緒的肩頭撒下一句:Done.
有些小獸膽怯,途中躲進例外的樹洞,留下一聲 Faulted 的嘆息,告訴我們命運總會有預料不到的分岔。
還有些小獸倔強,在荒野裡聽見取消的號角,牠們放下未完成的誓言,把 Canceled 留給遠方的呼喚。
我們常以為,寫好 WaitAll、WhenAny、Result 就能馴服每一個平行的流向。但其實,程式的世界裡總有些小獸,會在等待裡撒野,在分支裡流浪,把未完成的回聲留在你心底