1. 介紹
想像你的程式碼是一個超市的排隊隊伍。如果你呼叫同步方法,整個隊伍(你的執行緒)要等收銀員為一個顧客辦完才會處理下一個。如果方法是非同步的,你就把東西放到收銀台然後去做別的事,等收銀員結束會再叫你回來。
在 .NET 裡,非同步方法(帶有 async 的方法)就是在方法前加了一個特別的「標記」(async),允許在方法內使用 await 來對其他非同步操作(比如網路或檔案操作)做非阻塞等待,而不會鎖住主執行緒。正是這個「標記」把普通的同步商店變成了現代化的無隊列服務。
非同步方法的基本簽名
非同步方法必須在簽名裡加上修飾詞 async。這是最簡單的範例:
public async Task MyAsyncMethod()
{
// 你的非同步程式碼放這裡
}
非同步方法的返回類型:
| 返回類型 | 說明 | 範例 |
|---|---|---|
|
方法非同步執行,但不回傳結果 | |
|
方法非同步執行並回傳類型為 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 出現在 Main 和 MyFakeDownloadAsync。
- 操作符 await 只能在標記為 async 的方法內使用。
- 方法 Task.Delay(2000) 模擬了一個長時間操作(例如網路請求)。
- 出現 await 後執行緒不會被阻塞 — Main 的執行會「暫停」,直到延遲結束為止。
3. 非同步方法在底層如何運作?
當編譯器看到帶有 async 的方法時,它會把方法轉成一個「狀態機」,用來記住暫停點、區域變數,並在 awaited 的操作完成後繼續執行。
下面是一個非常簡化的流程圖:
+-----------------------------------------------------------+
| 呼叫非同步方法(例如 await X) |
+-------------------------+---------------------------------+
|
v
操作還沒完成?(不是 Task.Completed)
| |
| 是 | 否
v v
暫停執行, 直接回傳
記住上下文 結果(或拋出例外)
|
非同步操作完成...
|
v
狀態機「喚醒」方法,
執行從 await 之後的地方繼續
記住實作細節不是必須的:上下文(暫停點和區域變數)會被儲存,當操作完成時會自動恢復。
在哪裡可以(或不可以)使用 async 和 await
- 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?因為你無法知道這種方法何時完成,也無法正確捕捉其中拋出的例外。
返回 ValueTask 和 ValueTask<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); // 可以,但這裡沒有任何非同步性。
}
GO TO FULL VERSION