.NET 非同步基礎:例外處理與取消

  連載《深入淺出 .NET 10 非同步程式設計》,這是第 4 章的完整內容。


在理想的世界裡,每一次網路請求都會成功,每一個檔案都能順利讀取。但在現實世界中,網路會中斷,檔案會遺失,資料庫也可能逾時。一個穩健的應用程式,必須能妥善處理這些非預期狀況。除此之外,非同步操作通常不會立刻完成;如果使用者在資料下載完成前回上一頁,或者直接關閉了某個視窗,我們也應該能取消那個仍在背景進行的下載任務,以免白白浪費系統資源。


這就是本章要探討的兩大主題:例外處理(exception handling) 與 任務取消(cancellation)

接下來,這一章會聚焦在兩類最常見的非理想情境。本章的學習目標包括:

  • 如何使用 try-catch 語法來捕捉 await 拋出的例外。
  • 了解 AggregateException 的角色,以及 await 如何簡化它。
  • 掌握 .NET 中標準的任務取消模式:CancellationTokenSource 與 CancellationToken
  • 學會為非同步操作實作逾時(timeout)機制。

await 如何處理例外

async/await 最令人讚賞的設計之一,就是它讓非同步的例外處理,寫起來幾乎和傳統同步程式碼一樣直覺。這意味著我們仍然可以使用熟悉的 try-catch 區塊,來捕捉非同步工作拋出的錯誤。不過,裡面還是有幾個值得特別留意的細節。

先從一個最基本的情境開始。假設我們要執行一個可能失敗的非同步操作,例如嘗試從一個不存在的網址下載內容:

static readonly HttpClient httpClient = new HttpClient();

static async Task<string> DownloadPageAsync(string url)
{
    // HttpClient 在要求失敗或無法解析主機名稱時,
    // 可能拋出 HttpRequestException
    string content = await httpClient.GetStringAsync(url);
    return content;
}

當 GetStringAsync 執行時,如果因為連線問題而引發 HttpRequestException,該例外並不會立刻在當下爆發出來。相反地,這個例外會先被捕捉並儲存在回傳的 Task 物件中。此時,Task 的狀態會被標記為 Faulted(故障)。

這個例外會一直待在 Task 裡面,直到你的程式碼真正去 await 它。就在 await 的那一刻,先前儲存的例外才會被重新拋出(re-thrown)

也正因如此,我們可以很自然地用標準的 try-catch 區塊來處理它:

var url = "https://this-host-does-not-exist.invalid";
try
{
    string content = await DownloadPageAsync(url);
    Console.WriteLine("下載成功!");
}
catch (HttpRequestException ex)
{
    Console.WriteLine("發生網路錯誤:");
    Console.WriteLine(ex.Message);
}

執行結果:

發生網路錯誤:
No such host is known. (this-host-does-not-exist.invalid:443)

這個行為非常直觀:try-catch 區塊包住你認為可能出錯的 await 呼叫,就像處理同步程式碼一樣。至於背後那些複雜細節,則交給 async/await 的狀態機機制替我們處理。

原始碼: DemoExceptionHandling

小心「射後不理」的陷阱

這裡有一個很常見的陷阱。如果你啟動了一個非同步方法,卻既沒有 await 它,也沒有在之後觀察那個 Task,也就是所謂的「fire-and-forget」,那麼該方法拋出的例外就很容易變成「靜默失敗」。

public void Button_Click(object sender, EventArgs e)
{
    // ✘ 錯誤示範:沒有 await!
    // 若 SaveDataAsync 回傳 Task 並拋出例外,這裡完全捕捉不到,
    // 程式通常也不會崩潰。使用者會以為儲存成功了,但其實失敗了。
    _ = SaveDataAsync();
}

當一個 Task 發生錯誤卻沒有被等待時,例外會一直留在那個 Task 物件裡。使用 discard 運算子 (_) 雖然能消除編譯器的「未等待」警告,但它無法解決例外被吞掉的問題。雖然後台的 Garbage Collector(GC)在回收這個 Task 時,可能會觸發 TaskScheduler.UnobservedTaskException 事件,但在預設情況下,這不會導致程式崩潰,因此這類錯誤往往就變成難以察覺的「靜默失敗」。

換句話說,「先啟動,稍後再 await」本身不是問題;真正危險的是把 Task 直接丟掉,導致呼叫端再也不知道它最後是成功、失敗,還是被取消。

這裡討論的是回傳 Task 或 Task<T> 的非同步方法。若方法是 async void(例如某些 UI 事件處理器),例外不會被包進 Task 裡——它會被拋到當時的 SynchronizationContext(若有的話),或直接拋到 ThreadPool,通常會成為未處理例外而導致程式崩潰。因此,除了事件處理器等必要情境外,應避免使用 async void

關於 SynchronizationContext 的說明,請參閱第 3 章。

最佳實務

  1. 盡量避免 fire-and-forget:除非你有非常明確的理由,否則至少要在某個時間點 await 它,或以其他方式明確觀察它的完成結果。
  2. 如果必須 fire-and-forget:請使用 SafeFireAndForget 擴充方法(需自行實作或使用第三方套件)來記錄錯誤,或者在該非同步方法內部最外層包上 try-catch 確保例外不會外洩。

AggregateException 的角色

在探索 .NET 非同步 API 的過程中,你會遇到一個名為 AggregateException 的特殊例外類型。它的設計目的,是作為一個可以包裹一個或多個例外的容器

當你使用 Task.Wait() 或 Task.Result 這種阻塞式方法,去等待一個故障的 Task 時,原始例外,例如 HttpRequestException,會先被包在 AggregateException 裡,再一起拋出。參考以下範例:

 var faultyTask =
     DownloadPageAsync("https://this-host-does-not-exist.invalid");

try
{
    // 使用 .Wait() 會拋出 AggregateException
    faultyTask.Wait();
}
catch (AggregateException ex)
{
    // 我們需要從 .InnerExceptions 集合中解開真正的例外
    var realException = ex.InnerExceptions.First();
    Console.WriteLine(
        "捕捉到 AggregateException,真正的錯誤是:"
        + realException.GetType().Name);
}

執行結果:

捕捉到 AggregateException,真正的錯誤是:HttpRequestException

這裡故意使用同步阻塞的 Wait() 方法,是為了示範出錯的 Task 在非 await 情境下會以 AggregateException 包裝原始例外。實務上,當然還是應該優先使用 await,因為它通常會幫你解開(unwrap) AggregateException,直接傳播其中一個原始例外,而不是把整個 AggregateException 丟給你處理。

這也就是為什麼在本章前面那個 await 下載頁面的範例中,我們可以直接 catch (HttpRequestException ex)await 讓例外處理程式碼可以更專注,也更簡潔。

不過,AggregateException 在某些進階情境下還是會用到,例如使用 Task.WhenAll 等待多個可能同時失敗的任務時。

Task.WhenAll 與多重例外

當我們使用 Task.WhenAll 等待多個任務時,情況就會稍微複雜一點。如果多個任務都失敗了,Task.WhenAll 回傳的任務會包含所有發生的例外。

然而,當你 await 這個任務時,await 關鍵字只會傳播其中一個例外;其他例外仍然會保留在 Task.Exception 裡。

try
{
    var task1 = ThrowAsync("Error 1");
    var task2 = ThrowAsync("Error 2");
    await Task.WhenAll(task1, task2);
}
catch (Exception ex)
{
    // 這裡只會捕捉到其中一個錯誤
    Console.WriteLine($"捕捉到:{ex.Message}");
}

執行結果:

捕捉到:Error 1

如果你真的需要處理所有錯誤,就得檢查 Task 物件本身的 Exception 屬性(型別是 AggregateException):

var task1 = ThrowAsync("Error 1");
var task2 = ThrowAsync("Error 2");
var allTasks = Task.WhenAll(task1, task2);
try
{
    await allTasks;
}
catch
{
    // 檢查 allTasks.Exception 來取得所有錯誤
    foreach (var innerEx in allTasks.Exception!.InnerExceptions)
    {
        Console.WriteLine($"錯誤:{innerEx.Message}");
    }
}

執行結果:

錯誤:Error 1
錯誤:Error 2

原始碼: DemoAggregateException

此範例在 catch 區塊中的 foreach 陳述式,使用了 null 寬容運算子(null-forgiving operator)!,用意是告訴編譯器:「這裡我確定不會是 null,不用出警告。」若想進一步了解 null 寬容運算子,可參考微軟文件或 《現代 C#:AI 時代的開發者修煉》 第 3 章:空值安全。

Tip

在防禦性程式設計中,如果你真的很在意多筆併發操作(例如多筆資料庫寫入)中「每一次」的失敗原因,建議不要只看 await 拋出的第一個例外。你可以像上面的範例一樣,把 AggregateException 裡面的所有 InnerExceptions 都記錄(log)下來,這樣就不會漏掉其他連帶的錯誤資訊了。

延伸閱讀: 微軟文件 Consuming the Task-based Asynchronous Pattern 有更完整的 Task.WhenAll、例外聚合與 await 行為說明。

妥善取消任務

接下來,我們把焦點轉到另一個同樣重要的主題:取消。

.NET 提供了一套標準而清楚的協同式取消(cooperative cancellation)模式。所謂「協同式」,意思是取消操作不是粗暴地直接終止執行緒,而是由發起方「請求取消」,再由執行任務的一方「監聽請求並自行中止」。

這個模式由兩個核心類別組成:

  • System.Threading.CancellationTokenSource(CTS):負責建立與發出取消訊號的物件。可以把它想像成一個「取消按鈕」。
  • System.Threading.CancellationToken:負責傳遞與監聽取消訊號的物件。它是一個輕量的結構,代表一份可傳遞的取消訊號,會被傳遞給需要被取消的非同步方法。

把整體流程先記成三個步驟就好:建立 CTS → 傳遞 Token → 在工作端監聽取消訊號,並在呼叫端處理取消例外

接下來,我們就分別看看如何發出取消請求,以及如何監聽取消訊號。

發出取消請求

整個流程的起點,是先建立一個 CancellationTokenSource 物件(簡稱 CTS)。

// 1. 建立 CancellationTokenSource
using var cts = new CancellationTokenSource();

// 2. 從 CTS 取得 CancellationToken
var token = cts.Token;

// 3. 將 token 傳遞給你的非同步方法
// (此處僅展示「傳遞 token」,完整等待與例外處理見後續範例)
Task workTask = DoSomeLongRunningWorkAsync(token);

// 4. 在未來的某個時間點,當你決定要取消時...
Console.WriteLine("使用者決定取消操作!");
cts.Cancel(); // 按下「取消按鈕」

呼叫 cts.Cancel() 之後,取消訊號就會傳遞給所有從這個 CTS 取得的 CancellationToken

Note:

從 .NET 8 開始,CancellationTokenSource 也提供了 CancelAsync()。如果你需要「等待」所有註冊在 CancellationToken 上的回呼(callbacks)或相關取消通知流程都執行完畢,再繼續往下做其他事,就可以使用 await cts.CancelAsync()。本章這裡先使用 cts.Cancel(),是因為範例要聚焦在「發出取消請求」這個基本概念,而且這個範例本身也不需要等到取消回呼全部完成後,才能繼續往下處理。

監聽並向下傳遞取消訊號

另一方面,執行任務的非同步方法則需要把這個 CancellationToken 接收進來,並負責:

  1. 週期性檢查它(若這是一個含有迴圈或多個步驟的自訂方法)。
  2. 一路向下傳遞(pass-through)它給其他所有支援取消的底層方法(例如:資料庫呼叫、HTTP 請求或其他非同步方法)。

這也是 C# 非同步程式設計的一個極為重要的最佳實務:「讓 Token 一路傳下去」


下面這個範例把「持續檢查」和「向下傳遞」這兩件事放在一起示範。請特別注意:方法本身一方面會在迴圈中主動檢查是否取消,另一方面也會在呼叫 Task.Delay 時,繼續把同一個 token 傳下去。

static async Task DoSomeLongRunningWorkAsync(CancellationToken token)
{
    Console.WriteLine("背景工作已開始...");
    try
    {
        for (int i = 0; i < 10; i++)
        {
            // 檢查是否已經收到取消請求 (例如在運算密集的迴圈內)
            token.ThrowIfCancellationRequested();

            Console.WriteLine($"正在執行第 {i + 1}/10 部分的工作...");

            // 重要:將 token 繼續向下傳遞給任何支援的底層 API!
            await Task.Delay(1000, token);
        }
        Console.WriteLine("背景工作順利完成。");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("背景工作已被取消。");
        throw; // 通常需要繼續往外拋
    }
}

在非同步工作的內部,我們通常會在迴圈或關鍵步驟中使用 token.ThrowIfCancellationRequested() 來檢查 token 是否已被取消;如果已取消,就拋出 OperationCanceledException

ThrowIfCancellationRequested 會拋出 OperationCanceledException,而 Task.Delay 亦有可能拋出 OperationCanceledException 或其衍生型別 TaskCanceledException

在呼叫端,當我們 await 一個被取消的 Task 時,通常會看到 OperationCanceledException 或其衍生型別(例如 TaskCanceledException)被重新拋出。因此,我們就可以在呼叫端的 catch 區塊中捕捉它,進而知道任務是被取消,而不是因為其他錯誤而失敗。這通常是預期流程的一部分,不一定代表程式真的出錯。相對地,若背景工作拋出的是其他例外,就應該另外處理。以下範例把這整件事串起來:

// 1. 建立 CancellationTokenSource
using var cts = new CancellationTokenSource();

// 2. 從 CTS 取得 CancellationToken
var token = cts.Token;

// 3. 將 token 傳遞給你的非同步方法 (先不要立刻 await,讓它在背景跑)
Task workTask = DoSomeLongRunningWorkAsync(token);

// 模擬使用者操作了一段時間後決定取消
await Task.Delay(2500);

// 4. 在未來的某個時間點,當你決定要取消時...
Console.WriteLine("\n[呼叫端] 使用者決定取消操作!");
cts.Cancel(); // 按下「取消按鈕」

try
{
    await workTask; // 等待背景工作結束
}
catch (OperationCanceledException)
{
    // 這是預期的
    Console.WriteLine("呼叫端捕獲 OperationCanceledException。");
}
catch (Exception ex)
{
    // 這才是真正的錯誤
    Console.WriteLine($"工作發生異常:{ex.Message}");
}

執行結果:

背景工作已開始...
正在執行第 1/10 部分的工作...
正在執行第 2/10 部分的工作...
正在執行第 3/10 部分的工作...

[呼叫端] 使用者決定取消操作!
背景工作已被取消。
呼叫端捕獲 OperationCanceledException。

原始碼: DemoCancellationToken


 


Note: 許多 .NET 內建的非同步 API(例如 HttpClient 的方法、Task.Delay 等)都有接受 CancellationToken 的多載版本。盡可能使用它們,因為它們在底層已經為你處理了監聽取消訊號的邏輯。

值得一提的是,在較複雜的方法裡,取消可能不只一個來源。例如:

  • 情況 A: 呼叫端傳入的 token 被取消。
  • 情況 B: 內層某個 library 或元件因逾時而自行取消,並拋出 OperationCanceledException

此時,when 子句可以用來縮小 catch 的範圍,讓你只處理自己關心的取消情境。若你想判斷是否為呼叫端傳入的那個 token 觸發了取消,可以這樣寫:

catch (OperationCanceledException oce)
    when (oce.CancellationToken == token)
{
    Console.WriteLine("呼叫端已要求取消。");
}

不過要注意,這種比對方式並不是放諸四海皆準。若中途使用了 CreateLinkedTokenSource,或是底層 API 拋出的 OperationCanceledException 攜帶的是另一個 token,oce.CancellationToken 就不一定等於你原本傳入的那個 token。碰到多個取消來源時,通常更可靠的做法,是用稍後的逾時範例那樣檢查你自己持有的來源 token 是否已經 IsCancellationRequested

註冊取消回呼(cancellation callbacks)

有些時候,你使用的 API 可能不直接支援 CancellationToken,或者你需要在取消發生時順手做一些清理工作,例如關閉 socket、刪除暫存檔。這時候,就可以使用 CancellationToken.Register 方法。

using var registration = token.Register(() =>
{
    Console.WriteLine("收到取消通知,開始清理資源...");
    // 例如:關閉自訂連線、刪除暫存檔,或通知其他元件停止工作
});

// ... 執行操作 ...

Register 方法會回傳一個 CancellationTokenRegistration 物件,而它實作了 IDisposable。因此,當你使用 using 宣告或 using 區塊時,就能確保在操作完成後,或者取消回呼(cancellation callbacks)執行完畢後,釋放相關資源,這對避免記憶體洩漏非常重要。

實務上,cancellation callbacks 建議保持短小且可重入,而且應避免在 callback 裡執行耗時工作,以免拖慢或干擾取消流程。

Important: 註冊回呼的記憶體洩漏陷阱

這裡有個容易被忽略的細節:如果傳入的 CancellationToken 是來自一個長壽命(long-lived) 的來源(例如 ASP.NET Core 的 ApplicationStopping,或是存活於整個應用程式生命週期的全域 CTS),而我們在每個短暫的操作中呼叫 token.Register(...) 卻沒有把它 Dispose 掉,會發生什麼事呢?

答案是:你的 callback 委派(以及它所參照到的變數與物件)會被那個長壽命的 CTS 緊緊抓著不放。久而久之,這就會造成記憶體洩漏(memory leak)。所以,請務必養成好習慣,像範例中那樣使用 using 區塊,或是明確呼叫 .Dispose() 來註銷(unregister)回呼。

延伸閱讀: 微軟文件 Cancellation in Managed Threads 整理了 CancellationTokenRegister、linked token,以及取消回呼與 Dispose 的注意事項。

不可取消的 Token

有時候,你呼叫的方法雖然要求傳入 CancellationToken,但你其實不打算提供取消機制。這時候,就可以傳入 CancellationToken.None。像這樣:

// 明確表示:對此操作,我不打算提供取消機制
await DoSomethingAsync(CancellationToken.None);

這比傳入 default(CancellationToken) 更具可讀性,清楚地表達了你的意圖。

實作逾時機制

學會取消模式後,實作「逾時」(timeout)就會變得很自然。本質上,逾時就是在指定時間到達後自動發出取消請求。CancellationTokenSource 的建構函式有一個很方便的多載,可以在指定時間過後自動呼叫 Cancel()。先看下面這個範例:

// 建立一個 3 秒後會自動取消的 CTS
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

try
{
    Console.WriteLine("開始執行一個最多只能跑 3 秒的工作...");
    // 假設這個工作內部需要跑 10 秒以上
    await DoSomeLongRunningWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("工作因為逾時而被取消了!");
}

在這個範例中,DoSomeLongRunningWorkAsync 方法本身完全不需要知道「逾時」的存在,它只要像前面一樣監聽傳入的 CancellationToken 就可以了。我們只要建立一個會自動取消的 CTS,就能替這個操作加上逾時限制。這是一個非常強大,而且很好組合的模式。

不過,這裡之所以能直接把 catch (OperationCanceledException) 解讀成「逾時」,是因為範例裡只有一個取消來源,也就是這個會自動取消的 CTS。若你的 API 同時還接受外部傳入的 CancellationToken,就不能只看到 OperationCanceledException 就認定是 timeout;你需要像下一節那樣,進一步區分到底是逾時取消,還是呼叫端主動取消。

原始碼: DemoTimeouts

底下是範例程式 DemoTimeouts 的執行結果:

開始執行一個最多只能跑 3 秒的工作...
背景工作已開始...
正在執行第 1/10 部分的工作...
正在執行第 2/10 部分的工作...
正在執行第 3/10 部分的工作...
背景工作已被取消。
工作因為逾時而被取消了!

可測試的逾時機制:TimeProvider (.NET 8+)

這一節的重點可以先記成一句話:用 TimeProvider 把「逾時」從真實時間解耦,讓 timeout 邏輯可以快速而穩定地測試。

上面的逾時機制雖然簡單好用,但一到了單元測試就會碰到一個痛點:我們很難控制時間。如果測試要驗證「30 秒逾時」的邏輯,難道真的要讓測試跑滿 30 秒嗎?這會讓整個測試套件變得又慢又不穩定。

.NET 8 引入了 System.TimeProvider 抽象類別,正是為了解決這個問題。它允許我們在測試中「快轉」時間,或者精確控制時間的流逝。

此模式需要 .NET 8+;在測試端可搭配 Microsoft.Extensions.TimeProvider.Testing 套件(命名空間為 Microsoft.Extensions.Time.Testing)中的 FakeTimeProvider。使用時,第一步通常是透過依賴注入(dependency injection)或方法參數,把 TimeProvider 傳入服務中。在正式環境下,我們會注入 TimeProvider.System;而在測試時,則改注入 FakeTimeProvider

範例:

public class MyService(TimeProvider timeProvider)
{
   public async Task DoWorkWithTimeoutAsync(CancellationToken token)
   {
      // 使用 CancellationTokenSource 的建構函式來傳入 TimeProvider
      // 這與 new CancellationTokenSource(delay) 類似,但它是可測試的!
      var seconds = TimeSpan.FromSeconds(30);
      using var cts = new CancellationTokenSource(seconds, timeProvider);

      // 連結外部傳入的 token (如果外部取消,我們也要取消)
      using var linkedCts =
          CancellationTokenSource.CreateLinkedTokenSource(
              token, cts.Token);
      try
      {
         await DoActualWorkAsync(linkedCts.Token);
      }
      catch (OperationCanceledException)
          when (cts.Token.IsCancellationRequested
                && !token.IsCancellationRequested)
      {
         // 只有在 timeout token 已取消、
         // 且外部 token 尚未取消時,才視為逾時
         throw new TimeoutException("操作已逾時。");
      }
   }
}

這裡先用 CancellationTokenSource 的 CreateLinkedTokenSource 方法,把兩個取消訊號連結(合併)在一起:外部取消(token)與逾時取消(cts.Token)。這樣做的目的,是確保任一方觸發時都能停止工作。這個方法會回傳一個新的 CancellationTokenSource,並存放在 linkedCts 變數中。

接著,程式進入 try 區塊開始執行非同步工作。而 catch 的例外篩選條件,則限定只有在「timeout token 已取消、且外部 token 尚未取消」時,才會拋出 TimeoutException;如果是呼叫端主動取消,就保留原本的取消語意。

註:為了聚焦逾時與取消流程,上例省略了 DoActualWorkAsync 的實作細節;完整可編譯版本請參考本節附的範例專案。

到了單元測試中,就可以使用 Microsoft.Extensions.Time.Testing.FakeTimeProvider 傳入一個假的 TimeProvider,然後自由控制時間:

[Fact]
public async Task Should_Throw_TimeoutException_When_Time_Passes()
{
    // Arrange
    var fakeTime = new FakeTimeProvider();
    var service = new MyService(fakeTime);

    // Act
    var task = service.DoWorkWithTimeoutAsync(CancellationToken.None);

    // Assert: 此時任務應該還沒完成
    Assert.False(task.IsCompleted);

    // Act: 讓時間快轉 30 秒 + 1 tick
    fakeTime.Advance(TimeSpan.FromSeconds(30) + TimeSpan.FromTicks(1));

    // Assert: 任務應該因為逾時而拋出 TimeoutException
    await Assert.ThrowsAsync<TimeoutException>(() => task);
}

最後一個 assert 陳述句的意思,是這裡應該會拋出 TimeoutException。下面稍微拆開說明:

  • Assert.ThrowsAsync<TimeoutException>(...) 是 xUnit 的非同步斷言:預期參數中的動作會以 TimeoutException 失敗。
  • () => task 是個 Func<Task> 委派,把「要被驗證的工作」交給 ThrowsAsync
  • 外層 await 會等待斷言本身完成;若真的出現 TimeoutException,則測試通過。
  • 若 task 沒有拋出例外、或拋的是其他型別(例如 OperationCanceledException),測試會失敗。

透過 TimeProvider,我們不僅能寫出更穩健的逾時邏輯,也能讓相關測試在幾毫秒內執行完畢,而不必真的等待。這是 .NET 8 為所有需要處理時間的 API 帶來的一項很重要的改進。

原始碼: DemoTimeProviderTest

延伸閱讀: 微軟文件 What is TimeProvider?

結語

本章聚焦在非同步程式設計裡最常見的「非理想情境」。我們學會了如何用熟悉的 try-catch 處理非同步例外,也理解了 await 如何幫我們免去手動拆解 AggregateException 的負擔。

更重要的是,我們掌握了 .NET 標準的協同式取消模式。透過 CancellationTokenSource 與 CancellationToken,我們不但能讓使用者主動取消長時間任務,也能替非同步操作加入逾時保護。

讀到這裡,我們已經具備撰寫穩健非同步程式碼的重要基礎了:不只會處理成功路徑,也能穩當地面對失敗與取消。下一個挑戰則是,當多個非同步任務同時執行並共享資源時,該如何確保資料完整性與一致性;這正是下一章「執行緒同步與經典問題」要探討的主題。

👉 本文摘自 《深入淺出 .NET 10 非同步程式設計》的第 4 章完整內容。



本書內容連載到此結束。最後摘錄書中的幾張插圖:


如「作者序」所說,本書是以「理解優先」的方式編寫。書中的這些插圖,用意就是希望能降低理解負擔,並凸顯重點。

Keep learning!

沒有留言:

技術提供:Blogger.
回頂端⬆️