CodeGym /課程 /C# SELF /非同步方法的結構 ( async/

非同步方法的結構 ( async/ await)

C# SELF
等級 59 , 課堂 3
開放

1. 介紹

想像你的程式碼是一個超市的排隊隊伍。如果你呼叫同步方法,整個隊伍(你的執行緒)要等收銀員為一個顧客辦完才會處理下一個。如果方法是非同步的,你就把東西放到收銀台然後去做別的事,等收銀員結束會再叫你回來。

在 .NET 裡,非同步方法(帶有 async 的方法)就是在方法前加了一個特別的「標記」(async),允許在方法內使用 await 來對其他非同步操作(比如網路或檔案操作)做非阻塞等待,而不會鎖住主執行緒。正是這個「標記」把普通的同步商店變成了現代化的無隊列服務。

非同步方法的基本簽名

非同步方法必須在簽名裡加上修飾詞 async。這是最簡單的範例:

public async Task MyAsyncMethod()
{
    // 你的非同步程式碼放這裡
}

非同步方法的返回類型:

返回類型 說明 範例
Task
方法非同步執行,但不回傳結果
public async Task SaveFileAsync()
Task<T>
方法非同步執行並回傳類型為 T 的結果
public async Task<int> GetSumAsync()
void
只用在事件處理器。不建議在其他地方使用!
public async void Button_Click(...)
ValueTask / ValueTask<T>
用於高效能情境,當結果常常同步已就緒時

重要:除事件處理器外不要使用 async void!否則你可能會失去正常的錯誤處理。

2. 簡單的非同步方法範例

回到我們的練習應用(假設是一個一般的 console 程式),加入一個從網路取得資料的非同步方法。我們會模擬延遲。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("啟動非同步方法...");
        await MyFakeDownloadAsync();
        Console.WriteLine("非同步操作已完成!");
    }

    static async Task MyFakeDownloadAsync()
    {
        Console.WriteLine("開始下載(模擬 2 秒延遲)...");
        await Task.Delay(2000); // 模擬耗時操作
        Console.WriteLine("下載完成!");
    }
}

範例註解:

  • 關鍵字 async 出現在 MainMyFakeDownloadAsync
  • 操作符 await 只能在標記為 async 的方法內使用。
  • 方法 Task.Delay(2000) 模擬了一個長時間操作(例如網路請求)。
  • 出現 await 後執行緒不會被阻塞 — Main 的執行會「暫停」,直到延遲結束為止。

3. 非同步方法在底層如何運作?

當編譯器看到帶有 async 的方法時,它會把方法轉成一個「狀態機」,用來記住暫停點、區域變數,並在 awaited 的操作完成後繼續執行。

下面是一個非常簡化的流程圖:


+-----------------------------------------------------------+
|          呼叫非同步方法(例如 await X)                   |
+-------------------------+---------------------------------+
                          |
                          v
            操作還沒完成?(不是 Task.Completed)
                |                  |
                | 是               | 否
                v                  v
          暫停執行,             直接回傳
          記住上下文            結果(或拋出例外)
                |
      非同步操作完成...
                |
                v
        狀態機「喚醒」方法,
        執行從 await 之後的地方繼續

記住實作細節不是必須的:上下文(暫停點和區域變數)會被儲存,當操作完成時會自動恢復。

在哪裡可以(或不可以)使用 asyncawait

  • async 放在方法(或 lambda)的宣告前。
  • async 方法內可以(也應該)使用 await
  • 如果在沒有 async 的方法裡寫 await — 編譯器會報錯。
  • 如果加了 async,但方法內沒有任何 await — 會有警告;方法會同步執行並回傳已完成的 task。

4. 返回值類型與特性

返回 Task

如果方法做非同步工作但不回傳值,使用 Task

public async Task SaveToFileAsync(string path)
{
    await Task.Delay(1000);
    // 寫入檔案之類的
}

返回 Task<T>

需要回傳結果時,使用 Task<T>

public async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 42;
}

呼叫方式:

int result = await CalculateAsync();

返回 async void

僅供事件使用!例如按鈕點擊的處理器:

private async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
    // 做其他事
}

為什麼不要在一般方法使用 async void?因為你無法知道這種方法何時完成,也無法正確捕捉其中拋出的例外。

返回 ValueTaskValueTask<T>

這類型為高效能函式庫設計,當結果很多時候是同步就緒時可以避免額外的 task 分配。剛開始學習時可以不用太常用:平常不會常見。

5. 範例:回傳結果的非同步計算器

假設程式要「延遲」計算兩個數字的總和,就好像那是很複雜的運算:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("請輸入第一個數字:");
        int a = int.Parse(Console.ReadLine()!);

        Console.WriteLine("請輸入第二個數字:");
        int b = int.Parse(Console.ReadLine()!);

        Console.WriteLine("執行計算...");
        int sum = await CalculateSumAsync(a, b);
        Console.WriteLine($"總和: {sum}");
    }

    static async Task<int> CalculateSumAsync(int x, int y)
    {
        await Task.Delay(2000); // 「耗時」操作
        return x + y;
    }
}

重要的點:

  • CalculateSumAsync 是非同步方法,回傳 Task<int>,內部有 await
  • Main 中用 await 呼叫(提醒:從 C# 7.1 開始 Main 可以是非同步的)。
  • 計算總和的時候執行緒不會被阻塞 — 可以顯示進度或做其他工作。

6. 嵌套方法與嵌套 await

非同步方法可以呼叫另一個非同步方法,那個方法又可以呼叫下一個。整個鏈會自動繼續:

public async Task<int> StartWorkAsync()
{
    int data = await GetDataAsync();
    int result = await ProcessDataAsync(data);
    return result;
}

很簡單:等一個操作完成,再等下一個。

7. 陷阱與常見錯誤

1. 別忘了加 async
如果忘記 async,編譯器會在方法內看到 await 時報錯,無法編譯。

2. 不要在一般方法使用 async void
非同步的 void 方法(除了事件處理器)是例外的「黑洞」。裡面的錯誤可能會悄悄消失,你甚至不知道發生了什麼。

3. 非同步方法但沒用到 await
可以這樣做,但沒意義 — 方法會同步執行並回傳已完成的 task。

public async Task DoNothingAsync()
{
    // 哎呀,沒有任何 await!
}

4. 從非非同步方法回傳 Task
有時為了一致性的介面會這樣做:部分操作是真正非同步的,部分是瞬間完成的。

public Task<int> Foo()
{
    // return 42; // 這樣不行!
    return Task.FromResult(42); // 可以,但這裡沒有任何非同步性。
}
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION