第二章:C#非同步程式設計簡介

平元兄發表於2024-12-08

第二章:非同步程式設計簡介 🚀

目錄
  • 第二章:非同步程式設計簡介 🚀
    • 2.1 非同步程式設計的基本概念 🤔
      • 同步 vs 非同步
      • 非同步程式設計的優勢 🌟
    • 2.2 C# 中的非同步程式設計模型簡介 🛠️
      • 非同步程式設計模型(APM)
      • 基於事件的非同步模式(EAP)
      • 基於任務的非同步模式(TAP)
        • asyncawait 關鍵字 🔑
    • 示例程式碼
      • 基於回撥(Callback)非同步程式設計模型(APM) 🧩
        • 回撥地獄(Callback Hell)🔥
        • 回撥地獄示例
      • 基於事件的非同步模式(EAP) 📅
        • 示例程式碼:WebClient 的 EAP 模式
        • EAP 的問題
      • 基於任務的非同步模式(TAP)
        • 使用 async/await 重寫回撥示例
    • 小結:為什麼選擇 async/await? 💡


非同步程式設計(Asynchronous Programming)是現代程式設計中處理 I/O 密集型任務的關鍵技術之一。與傳統的同步程式設計不同,非同步程式設計允許程式在等待耗時操作(如網路請求、檔案讀寫、資料庫查詢等)完成時,繼續執行其他任務,從而提高應用程式的響應性和效率。

在本章中,我們將介紹非同步程式設計的基本概念、C# 中的非同步程式設計模型以及常用的 asyncawait 關鍵字。我們還會探討非同步程式設計的一些常見應用場景和最佳實踐,幫助你理解如何在實際開發中使用非同步程式設計來提升效能。

2.1 非同步程式設計的基本概念 🤔

同步 vs 非同步

  • 同步程式設計 🛑:在同步程式設計中,任務是順序執行的。如果某個操作是耗時的(如網路請求),程式會阻塞等待操作完成,之後才會繼續執行下一行程式碼。這種方式簡單直觀,但在處理 I/O 密集型任務時,可能會導致程式變得不響應。
  • 非同步程式設計 🔄:非同步程式設計允許程式在執行耗時操作時,不必等待操作完成,而是立即返回繼續執行其他任務。耗時操作完成後,會透過回撥、事件或 Task 通知程式結果。這樣,程式不會被阻塞,能更好地利用系統資源。

非同步程式設計的優勢 🌟

  1. 提高效能和吞吐量:非同步程式設計避免了執行緒的阻塞,使得應用程式可以處理更多的任務。
  2. 提升使用者體驗:對於 GUI 應用程式,非同步程式設計能保持介面的響應性,避免“卡頓”現象。
  3. 更好地利用資源:非同步程式設計通常使用執行緒池來管理後臺任務,避免了頻繁建立和銷燬執行緒的開銷。

2.2 C# 中的非同步程式設計模型簡介 🛠️

C# 提供了多種方式來實現非同步程式設計,其中最常用的是基於 Taskasync/await 的非同步程式設計模型。在此之前,C# 中還有以下非同步程式設計模型:

非同步程式設計模型(APM)

APM(Asynchronous Programming Model) 是 C# 中最早引入的非同步程式設計模式,通常使用 BeginXXX 和 EndXXX 方法來表示非同步操作。BeginXXX 方法啟動非同步操作,而 EndXXX 方法用於獲取操作結果。APM 使用 回撥 或 IAsyncResult 來處理非同步任務的完成。雖然簡單,但程式碼往往難以閱讀和維護,容易形成“回撥地獄”。

基於事件的非同步模式(EAP)

EAP(Event-based Asynchronous Pattern,EAP)是一種較早使用的非同步程式設計模式。它主要用於處理需要長時間執行的操作(例如 I/O 操作),並透過事件通知呼叫者操作的完成情況。這種模型曾經在 .NET Framework 2.0 和 3.5 中被廣泛使用,隨著 async/await 引入後,EAP 逐漸被淘汰。

基於任務的非同步模式(TAP)

C# 5.0 引入了 Taskasync/await,這是一種更加現代和簡潔的非同步程式設計方式,以同步編碼的方式編寫非同步程式碼。

asyncawait 關鍵字 🔑

  • async:用於標記一個方法為非同步方法。非同步方法通常返回 TaskTask<T>,表示非同步操作的結果。
  • await:用於等待一個非同步操作的完成,並在操作完成後繼續執行後續程式碼。await 會釋放當前執行緒,讓它去處理其他任務,避免了阻塞。

APMEAP基本已經被淘汰了,在本系列文章後續也不會再過多介紹。詳情參考官網

示例程式碼

基於回撥(Callback)非同步程式設計模型(APM) 🧩

回撥 是一種簡單的非同步程式設計方式,通常透過委託或匿名方法來實現。開發者將一個方法作為引數傳遞給非同步操作,當操作完成時,呼叫此方法返回結果。

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("開始非同步操作...");
        PerformAsyncOperation(ResultCallback);
        Console.WriteLine("主執行緒繼續執行...");

        // 防止程式過早結束
        Thread.Sleep(3000);
    }

    // 模擬非同步操作
    static void PerformAsyncOperation(Action<string> callback)
    {
        new Thread(() =>
        {
            Thread.Sleep(2000); // 模擬耗時操作
            callback("操作完成,結果為:42");
        }).Start();
    }

    // 回撥方法
    static void ResultCallback(string result)
    {
        Console.WriteLine(result);
    }
}

輸出:

開始非同步操作...
主執行緒繼續執行...
操作完成,結果為:42

解釋:

  • PerformAsyncOperation 方法啟動了一個新執行緒,並在操作完成後呼叫回撥方法 ResultCallback
  • 主執行緒不會被阻塞,能夠繼續執行其他操作。
  • 回撥方法用於處理非同步操作的結果。

回撥地獄(Callback Hell)🔥

回撥地獄是指當多個非同步操作依次依賴回撥時,程式碼會形成巢狀的結構,導致難以維護和閱讀。這種問題在複雜場景中尤為常見。

回撥地獄示例

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("開始非同步任務鏈...");
        Step1(result1 =>
        {
            Console.WriteLine(result1);
            Step2(result2 =>
            {
                Console.WriteLine(result2);
                Step3(result3 =>
                {
                    Console.WriteLine(result3);
                    Console.WriteLine("所有步驟完成!");
                });
            });
        });

        // 防止程式過早結束
        Thread.Sleep(5000);
    }

    static void Step1(Action<string> callback)
    {
        new Thread(() => { Thread.Sleep(1000); callback("步驟 1 完成"); }).Start();
    }

    static void Step2(Action<string> callback)
    {
        new Thread(() => { Thread.Sleep(1000); callback("步驟 2 完成"); }).Start();
    }

    static void Step3(Action<string> callback)
    {
        new Thread(() => { Thread.Sleep(1000); callback("步驟 3 完成"); }).Start();
    }
}

輸出:

開始非同步任務鏈...
步驟 1 完成
步驟 2 完成
步驟 3 完成
所有步驟完成!

問題:

  • 巢狀的回撥導致程式碼呈現“金字塔”結構,難以閱讀和維護。
  • 異常處理複雜,容易遺漏錯誤處理邏輯。

基於事件的非同步模式(EAP) 📅

EAP 是 C# 中較早的一種非同步程式設計模型,它使用 事件 來通知非同步操作的完成狀態。EAP 模式通常以 BeginXXXEndXXX 命名方法,或者透過事件處理完成通知。

示例程式碼:WebClient 的 EAP 模式

using System;
using System.Net;

class Program
{
    static void Main(string[] args)
    {
        using (WebClient client = new WebClient())
        {
            client.DownloadStringCompleted += OnDownloadStringCompleted;
            Console.WriteLine("開始下載資料...");
            client.DownloadStringAsync(new Uri("https://www.example.com"));

            // 防止程式過早結束
            Console.ReadLine();
        }
    }

    // 下載完成時觸發的事件處理程式
    static void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
    {
        if (e.Error != null)
        {
            Console.WriteLine($"下載失敗:{e.Error.Message}");
            return;
        }

        Console.WriteLine("下載成功,資料長度:" + e.Result.Length);
    }
}

輸出:

開始下載資料...
下載成功,資料長度:1270

解釋:

  • DownloadStringAsync 方法啟動非同步下載操作。
  • 當下載完成後,會觸發 DownloadStringCompleted 事件,並呼叫事件處理程式 OnDownloadStringCompleted
  • e.Result 包含下載的資料結果。

EAP 的問題

  • 複雜性:每個非同步操作都需要定義事件和處理程式,增加了程式碼的複雜性。
  • 錯誤處理困難:異常處理需要在事件處理程式中顯式檢查 Error 屬性。
  • 可維護性差:對於大量非同步操作,程式碼的結構會變得凌亂。

基於任務的非同步模式(TAP)

使用 async/await 重寫回撥示例

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("開始非同步任務鏈...");
        await Step1();
        await Step2();
        await Step3();
        Console.WriteLine("所有步驟完成!");
    }

    static async Task Step1()
    {
        await Task.Delay(1000);
        Console.WriteLine("步驟 1 完成");
    }

    static async Task Step2()
    {
        await Task.Delay(1000);
        Console.WriteLine("步驟 2 完成");
    }

    static async Task Step3()
    {
        await Task.Delay(1000);
        Console.WriteLine("步驟 3 完成");
    }
}

優勢:

  • 程式碼結構更加清晰,不再有深層巢狀。
  • 異常處理可以使用標準的 try/catch 塊。
  • 非同步方法的呼叫和同步方法類似,降低了編寫和理解非同步程式碼的難度。

解釋:

  • GetDataAsync 方法被標記為 async,表示這是一個非同步方法。
  • await 關鍵字等待 GetStringAsync 方法完成,而不會阻塞主執行緒。
  • 當網路請求完成後,response 字串會返回給呼叫者,程式繼續執行。

小結:為什麼選擇 async/await? 💡

隨著 C# 5.0 引入 async/await 關鍵字,非同步程式設計變得更加簡潔和直觀。相比於回撥和 EAP 模式,async/await 提供了更高層次的抽象,能夠以類似於同步程式碼的方式編寫非同步邏輯,極大地提升了程式碼的可讀性和維護性。

相關文章