CodeGym /課程 /C# SELF /關鍵字 async 和

關鍵字 async 和 await

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

1. 介紹

在跳進程式碼之前,我們先看動機並想像一個典型情況。假設你的程式要從網路下載一個檔案:

// 偽代碼
var data = DownloadFile("https://example.com/file");
ProcessData(data);

問題是:在下載進行時程式會「凍住」。其他任何操作都沒辦法執行 — 使用者不能移動滑鼠、按按鈕,甚至只能面對那個看起來卡住的視窗。

以前(或在別的語言)為了處理這種情況,常會用執行緒 (Thread)、任務 (Task)、委派 (delegates)、計時器等等 —— 這會讓程式碼越堆越多,變得難讀難維護。C# 團隊想讓生活更簡單:引入了用 async 和 await 的非同步機制。現在可以寫出幾乎跟同步程式碼一樣簡潔的非同步邏輯。

沒用 async/await 的經典「非同步痛苦」

為了對比,我們看看如果用執行緒手動處理長時間操作,為了不阻塞介面會怎麼寫:

// 沒有 async/await 的範例,手動處理
var thread = new Thread(() =>
{
    var data = DownloadFile("https://example.com/file");
    Console.WriteLine("檔案已下載!");
});
thread.Start();

這種做法相當笨重:你要自己管理執行緒,沒有簡單的方式「等待」結果,例外處理也很難搞。

2. C# 的非同步語法

定義:什麼是 async 和 await?

async — 是一個修飾詞,讓方法變成非同步的。這樣的方法通常會回傳 Task(或 Task<T>)、或 ValueTask。這是一個承諾:結果會到,但稍後才會回來(就像在網路商店下單 — 下了單就等)。

await — 是一個運算子,意思是:「在這一行遇到 await 時暫停。等到這個操作完成,但不要阻塞執行緒!其他事可以先做。」

非同步方法長什麼樣子?

public async Task MyAsyncMethod()
{
    Console.WriteLine("開始下載檔案...");
    var data = await DownloadFileAsync("https://example.com/file"); // 非同步等待結果!
    Console.WriteLine("完成!");
}

注意:

  • 在方法上加了修飾詞 async。
  • 方法內使用 await 來等待非同步操作。
  • 方法回傳 Task(如果有回傳值則回傳 Task<T>)。

視覺化:呼叫 async 方法時發生了什麼?

graph LR
A[呼叫 MyAsyncMethod] --> B[執行到 await 之前]
B --> C[呼叫 DownloadFileAsync]
C --> D{等待}
D --> |檔案尚未下載| E[釋放執行緒]
D --> |檔案已下載| F[在 await 之後繼續執行]
F --> G[方法完成]
  • 在第一個 await 之前,方法是同步執行的。
  • 遇到 await 時方法會暫停,控制權回到呼叫端。
  • 當非同步操作完成後,執行會在 await 之後繼續——看起來就像沒發生什麼事一樣。

3. 範例:用 async/await 做非同步下載

假設我們的教學應用要從網路下載文字並印出長度,同時不阻塞其它程式流程。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // 非同步方法(回傳 Task)
    public static async Task DownloadAndPrintLengthAsync(string url)
    {
        Console.WriteLine("開始下載...");

        // 使用 HttpClient — 它支援非同步方法
        using (var client = new HttpClient())
        {
            string data = await client.GetStringAsync(url);
            Console.WriteLine($"下載完成!文字長度:{data.Length} 個字元。");
        }

        Console.WriteLine("方法已完成工作。");
    }

    static void Main()
    {
        // 啟動非同步操作並等它完成
        var task = DownloadAndPrintLengthAsync("https://www.example.com");
        task.Wait(); // 對簡單的 console 範例可行。在實際 UI 或 web 應用中這樣會阻塞並可能導致 deadlock。
    }
}

說明:

  • DownloadAndPrintLengthAsync — 完全非同步,靠 async 和 await 實現。
  • 在方法裡我們用 await 等待非同步下載字串完成。
  • 在 Main() 我們啟動任務並用 Wait() 明確等待完成。在現代 C# 可以把 Main 也做成非同步的(async Task Main),就能直接用 await。

4. 它是怎麼運作的?

同步與非同步程式碼的差別

同步 範例

Console.WriteLine("開始");
string data = client.GetStringAsync(url).Result; // .Result 會阻塞執行緒!
Console.WriteLine("操作完成");

非同步 範例

Console.WriteLine("開始");
string data = await client.GetStringAsync(url); // 執行緒不會被阻塞
Console.WriteLine("操作完成");

await 在「底層」如何運作?

當你使用 await 時,C# 會自動把你的方法「切分」成兩段(或更多段):在 await 之前的部分,和之後的部分。當被呼叫的非同步方法回傳一個 Task,你的方法會先把控制權交還給呼叫者;等到那個 Task 完成時,執行會在 await 之後繼續。這些都自動處理,你不需要手動管理執行緒或切換。

有趣的事:C# 會把你的非同步方法「轉變」成一個狀態機,每個 await 就是一個新的「回復點」。

在真實應用中使用 async/await

static async Task Main(string[] args)
{
    var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
    // 在下載時,我們可以做其他事情
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"工作中... 迭代 {i}");
        await Task.Delay(500); // 暫停 0.5 秒,模擬工作
    }
    await downloadTask; // 等待下載完成
}

現在程式既在下載東西,又不會被阻塞:活生生的多工,好比貓一邊睡一邊盯著牠的食碗。

非同步 ≠ 多執行緒

一個常被新手搞混的重點:非同步程式碼 可以 在同一個執行緒上執行!非同步的重點是不要阻塞執行緒,而不是一定要創建新執行緒。作業系統的執行緒是昂貴的資源!非同步允許你「放開」執行緒去做其他事,不用等那些耗時的操作(網路、檔案、計時器等)。

比較表:什麼情況選哪個?

情境 Thread/Task async/await
CPU-bound 工作 是 是(透過 Task.Run)
I/O-bound 工作 效率低下 很適合
大量平行 困難 簡單
程式碼簡潔度 困難 易讀

5. 有用的細節

非同步 Main

在現代的 C# 版本可以把 Main 做成非同步的!

static async Task Main(string[] args)
{
    // 你可以在 Main 裡直接 await 所有非同步程式碼
    await DownloadAndAnalyzeFileAsync("https://example.com/file");
}

常見錯誤:忘了 await — 任務「跑走」了

SomeAsyncFunction(); // 沒有 await,沒有人等它完成!

結果這個任務會在背景執行「自顧自」——如果發生例外,你可能完全不知道!

何時不該寫 async 方法

  • 如果方法內沒有任何非同步操作(根本沒有 await),就不要把它標記為 async。
  • 不要使用 async void(除非你是在實作事件處理器)。

簡短規則與常見問答

  • 可以在沒有 async 的方法裡使用 await 嗎? 不能!只能在被標記為 async 的方法內使用。
  • 一個方法可以有多個 await 嗎? 可以,數量不限——每個都是一個等待點。
  • 想要回傳結果怎麼辦? 使用 Task<T> 並用 return。
  • 可以合併多個非同步任務嗎? 當然可以!可以同時啟動多個任務,然後用 Task.WhenAll 一起等待它們。

6. 常見錯誤使用 async/await

錯誤 №1: "火球" — 在呼叫非同步任務後沒有 await。

DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("全部完成!"); // 其實下載還在進行中!

程式不會等待非同步操作完成。所有非同步任務要麼用 await,要麼明確用 Wait()(不過後者很危險,可能導致阻塞)。

錯誤 №2: 在同步與非同步之間混用 .Result 或 .Wait()。

這是個反模式,會把非同步的好處都破壞掉。在 UI 或 ASP.NET 應用中幾乎一定會導致 deadlock(互相等待造成死結):非同步任務等著執行緒釋放,而執行緒又被同步等待卡住。記住金科玉律: "async all the way"(一路 async 到頂層)。

錯誤 №3: 使用 async void。

async void 的方法無法被等待(不能用 await),而且從裡面丟出的例外無法用一般的 try-catch 捕捉,通常會導致應用程式崩潰。唯一合理的使用場景是事件處理器(例如 async void Button_Click(...)),那是因為簽名要求。其他情況請使用 async Task。

錯誤 №4: 在沒有 await 的方法上多加了 async。

如果你把方法標記為 async,但裡面根本沒有用到 await,編譯器會給警告。這種方法會完全同步執行,卻多出一個不必要的狀態機開銷。這會讓人誤會程式是非同步的,並降低效能。

2
任務
C# SELF, 等級 59, 課堂 2
上鎖
非同步方法與暫停
非同步方法與暫停
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION