iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 22 篇

Day 22|async / await:程式等待工作時,一定要卡住嗎?

  • 分享至 

  • xImage
  •  

Day 21 我們學會:

Method 無法正常完成
→ throw

有能力處理錯誤
→ catch

今天換另一個常見問題:

如果程式沒有出錯,只是在等待資料回來,該怎麼辦?

例如:

呼叫 API
查詢 Database
讀取 File
等待網路回應

這些工作都有共同流程:

開始工作
↓
等待
↓
取得結果

問題是:

等待期間,一定要讓目前的 Thread 卡在原地嗎?

這就是今天要學的:

Task
Task<T>
async
await

今天最重要的觀念:

await 會暫停目前 Async Method 後面的流程,但不代表把目前 Thread 一起阻塞住。


今天會學到

Blocking
→ 什麼叫做 Thread 被卡住?

await
→ 等待時到底會發生什麼?

Task
→ 如何表示一個尚未完成的操作?

Task<T>
→ 如何表示「之後會得到結果」的操作?

Execution Flow
→ Async Method 到底怎麼執行?

I/O-bound
→ 哪些工作適合使用 async / await?

先記住今天最重要的流程:

呼叫 Async Method
↓
Method 先開始執行
↓
遇到尚未完成的 await
↓
暫停目前 Method 的後續流程
↓
Task 完成
↓
繼續執行

1. 先看同步等待

先看:

Console.WriteLine("A");

Thread.Sleep(3000);

Console.WriteLine("B");

執行流程:

A
↓
Thread.Sleep(3000)
↓
Thread 停住約 3 秒
↓
B

Thread.Sleep() 會阻塞目前 Thread。

這稱為:

Blocking
→ 阻塞

也就是:

程式要等
+
Thread 也一起等

2. 改成非同步等待

改成:

Console.WriteLine("A");

await Task.Delay(3000);

Console.WriteLine("B");

結果看起來仍然是:

A

等待約 3 秒

B

但等待方式不同。

同步等待 非同步等待
Thread.Sleep() await Task.Delay()
阻塞目前 Thread 不需要同步阻塞目前 Thread
Thread 卡著等待 Task 完成後再繼續

所以:

非同步不是讓 3 秒消失,而是改變等待這 3 秒的方式。

今天使用 Task.Delay(),只是方便觀察非同步流程。

實務上更常等待的是:

HTTP
Database
File
Network

3. await 後面的程式會先執行嗎?

看:

Console.WriteLine("A");

await Task.Delay(3000);

Console.WriteLine("B");

很容易誤以為:

A
↓
Task.Delay 丟到背景
↓
先執行 B
↓
3 秒後再處理 Task

不是。

真正的流程是:

A
↓
Task.Delay()
↓
await
↓
Task 尚未完成
↓
目前 Method 暫停
↓
Task 完成
↓
繼續執行
↓
B

所以:

await 後面的程式,不會在 Task 尚未完成時偷偷先執行。


4. 那這樣不是還是「卡住」嗎?

這裡要分清楚兩件事:

Method 暫停

和:

Thread 被阻塞

不是同一件事。

看:

Console.WriteLine("A");

await Task.Delay(3000);

Console.WriteLine("B");

這裡確實是:

目前 Method
↓
停在 await
↓
B 要等 Task 完成後才能執行

但不代表:

目前 Thread
↓
也要一起卡 3 秒

所以:

Method 暫停
≠
Thread 阻塞

最重要的一句:

await 會暫停目前 Method 的後續流程,但不等於同步阻塞 Thread。


5. Task 是什麼?

現在再來看:

Task

Task 可以先理解成:

一個可能還沒有完成的操作。

例如:

Task task =
    Task.Delay(3000);

這裡的:

task

代表的是:

「等待 3 秒」這個操作

它可能:

還在進行

也可能:

已經完成

所以:

Task
→ 代表一個操作

而不是:

Task
→ 一條新的 Thread

這兩個概念不要混在一起。


6. 先從一般 Method 理解「結果」

先不要急著看 Task<T>。

先看一般 Method:

string GetMessage()
{
    return "Hello";
}

呼叫:

string message =
    GetMessage();

流程很直接:

呼叫 GetMessage()
↓
Method 執行完成
↓
得到 "Hello"

所以:

GetMessage()

得到的是:

string

7. 如果結果不能立刻拿到呢?

現在假設:

取得訊息需要等待 1 秒。

可以寫成:

async Task<string> GetMessageAsync()
{
    await Task.Delay(1000);

    return "Hello";
}

這次呼叫:

GetMessageAsync();

不能立刻得到:

"Hello"

因為操作還沒有完成。

所以 C# 先給我們:

Task<string>

可以先理解成:

一個「完成之後會得到 string」的操作。

也就是:

現在
↓
Task<string>

完成後
↓
string
↓
"Hello"

8. Task<T> 是「之後會得到 T」

所以:

Task<string> task =
    GetMessageAsync();

可以把它理解成:

task
→ 正在取得訊息的操作

現在還不是:

"Hello"

真正需要結果時:

string message =
    await task;

這裡的:

await task

可以理解成:

等待這個操作完成,然後把最後的結果給我。

所以:

Task<string>
↓
await
↓
string
↓
"Hello"

用一句話理解

Task<T>
→ 之後會得到 T

例如:

Task<string>
→ 之後會得到 string
Task<int>
→ 之後會得到 int
Task<Order>
→ 之後會得到 Order

Task 和 Task<T>

整理:

型別 意義
Task 操作完成後沒有額外結果值
Task<T> 操作完成後會得到 T

例如:

Task
→ 等工作完成
Task<string>
→ 等工作完成
→ 得到 string

最重要的型別關係:

Task<T>
↓
await
↓
T

9. async 是什麼?

看:

async Task<string> GetMessageAsync()
{
    await Task.Delay(1000);

    return "Hello";
}

前面的:

async

可以先理解成:

讓這個 Method 可以使用 await 撰寫非同步流程。

但不要理解成:

加 async
→ 自動建立背景 Thread

也不要理解成:

加 async
→ 程式自動變快

async 本身不會讓工作自動加速。


10. await 是什麼?

await

就是:

等待一個 Task 完成。

例如:

await Task.Delay(1000);

如果 Task 尚未完成:

目前 Method 暫停
↓
等待 Task 完成
↓
Task 完成
↓
繼續執行

如果 Task 本來就已經完成:

await
↓
直接繼續

所以:

只有當 Task 尚未完成時,await 才需要暫停目前 Method。


11. Async Method 到底怎麼執行?

現在來看今天最重要的範例。

Console.WriteLine("1");

Task task =
    WaitAsync();

Console.WriteLine("2");

await task;

Console.WriteLine("3");

async Task WaitAsync()
{
    Console.WriteLine("A");

    await Task.Delay(1000);

    Console.WriteLine("B");
}

請先猜輸出順序。

答案是:

1
A
2
B
3

為什麼?


第一步

先執行:

Console.WriteLine("1");

輸出:

1

第二步

接著:

Task task =
    WaitAsync();

WaitAsync() 不會整個被丟到背景。

它會先開始執行:

Console.WriteLine("A");

所以輸出:

A

目前:

1
A

第三步

接著 WaitAsync() 執行:

await Task.Delay(1000);

因為 Task 尚未完成:

WaitAsync()
↓
暫停

接著把:

Task

交回呼叫端。


第四步

呼叫端繼續:

Console.WriteLine("2");

所以現在:

1
A
2

第五步

接著:

await task;

呼叫端需要等待 WaitAsync() 完成。

大約 1 秒後:

Task.Delay()
↓
完成

WaitAsync() 繼續:

Console.WriteLine("B");

輸出:

B

接著 WaitAsync() 完成。

呼叫端的:

await task;

也完成。

最後:

Console.WriteLine("3");

輸出:

3

完整結果:

1
A
2
B
3

12. Async Method 不是「整個丟到背景」

剛才的範例非常重要。

這行:

Task task =
    WaitAsync();

不是:

WaitAsync()
↓
整個丟到背景
↓
立刻執行下一行

而是:

呼叫 WaitAsync()
↓
WaitAsync 先開始執行
↓
輸出 A
↓
遇到尚未完成的 await
↓
WaitAsync 暫停
↓
呼叫端繼續

所以:

Async Method 會先正常執行,直到遇到尚未完成的 await,才暫停後續流程。


13. 直接 await 和晚點 await 有什麼不同?

這個差異非常重要。


情況一:直接 await

await WaitAsync();

Console.WriteLine(
    "其他工作"
);

流程:

WaitAsync()
↓
等待完成
↓
其他工作

也就是:

後面的程式要等 WaitAsync() 完成。


情況二:先取得 Task

Task task =
    WaitAsync();

Console.WriteLine(
    "其他工作"
);

await task;

流程:

開始 WaitAsync()
↓
取得 Task
↓
先執行其他工作
↓
await task
↓
如果還沒完成就等待

所以:

開始一個非同步操作,和等待它完成,不一定要發生在同一個時間點。

這個觀念之後學:

Task.WhenAll()

會非常重要。


14. 和 JavaScript 的概念其實很接近

如果有 JavaScript 經驗,可以先這樣對照:

JavaScript Promise
≈
C# Task

JavaScript:

const promise = getData();

console.log("其他工作");

const result =
    await promise;

C#:

Task<string> task =
    GetDataAsync();

Console.WriteLine(
    "其他工作"
);

string result =
    await task;

兩者概念都很像:

啟動非同步操作
↓
取得代表未完成操作的物件
↓
先做其他事情
↓
await
↓
取得結果

但 C# 的 Runtime 執行模型與 JavaScript 不同。

今天不用深入底層,只需要知道:

C# 的 await 也不是單純把工作丟到背景 Thread。


15. 真正呼叫 API 時會發生什麼?

前面使用:

Task.Delay()

只是模擬等待。

實務上可能寫:

string data =
    await httpClient.GetStringAsync(
        "https://example.com/api/data"
    );

可以理解成:

送出 HTTP Request
↓
等待 Response
↓
await
↓
目前 Method 暫停
↓
等待期間不需要讓目前 Thread 一直阻塞
↓
Response 回來
↓
Task 完成
↓
得到 data
↓
繼續執行

不要理解成:

開一條背景 Thread
↓
讓那條 Thread 一直等 API

對真正的非同步 I/O,更接近:

送出 I/O
↓
等待外部結果
↓
不需要一條 Thread 一直坐著等
↓
I/O 完成
↓
Task 完成
↓
繼續執行

16. 為什麼 ASP.NET Core 很常使用 async / await?

假設 Server 同時收到:

Request A
Request B
Request C

每個 Request 都要查 Database。

如果同步等待:

Request A
→ Thread 卡著等 DB

Request B
→ Thread 卡著等 DB

Request C
→ Thread 卡著等 DB

就會有很多 Thread 花時間:

只是等待

如果使用非同步 I/O:

Request A
↓
送出 DB Query
↓
await
↓
Request A 暫停
↓
Thread 不需要一直陪著等

等 Database 回來後,再繼續處理 Request A。

所以:

非同步不是讓 Request 不用等,而是讓等待期間不必浪費一條 Thread 陪它一起等。


17. 哪些工作適合 async / await?

最典型的是:

I/O-bound
→ I/O 等待型工作

例如:

HTTP API
Database
File
Network

常見:


await httpClient.GetAsync(...);

await dbContext.SaveChangesAsync();

await query.ToListAsync();

await stream.ReadAsync(...);

這些工作的共同特色:

開始工作
↓
大量時間在等待外部資源
↓
取得結果

一般 CPU 計算呢?

例如:

int result =
    10 + 20;

或:

decimal total =
    orders.Sum(
        order => order.Price
    );

通常直接執行即可。

可以先記:

有非同步 API 的 I/O 等待
→ 適合 async / await

一般 CPU 計算
→ 通常直接執行

不要因為學了 async,就把所有 Method 都改成 Async Method。


18. async / await 不代表什麼?

不要把:
async / await

理解成:自動建立背景 Thread
也不要理解成:工作本身自動變快

假設 API 原本需要:2 秒

改成:await GetDataAsync();

API 還是可能需要:2 秒

真正改善的是:等待方式

所以:

async / await
≠
自動多執行緒



async / await
≠
自動加速工作



async / await
→
更有效率地處理等待

19. 和 Day 21 的 Exception 有什麼關係?

Async Method 一樣可能失敗。

例如:

try
{
    string data =
        await GetDataAsync();
}
catch (Exception ex)
{
    Console.WriteLine(
        ex.Message
    );
}

所以:

Exception Handling
+
async / await

仍然可以一起使用。

今天先知道:

被 await 的非同步操作失敗時,一樣可以進入 Exception Handling 流程。

詳細的 Async Exception Handling 留到 Day 23。


20. async / await 的 Mental Model

今天可以整理成:

呼叫 Async Method
        ↓
Method 開始執行
        ↓
遇到 await
        ↓
Task 已完成?
   ↙           ↘
 是             否
 ↓              ↓
直接繼續      暫停目前 Method
                ↓
        Thread 不需要卡著等
                ↓
            Task 完成
                ↓
             繼續執行

另外:

Task<T>
↓
await
↓
T

以及最容易搞混的:

Method 暫停
≠
Thread 阻塞

今日練習

練習一|同步等待與非同步等待

比較:

Thread.Sleep(2000);

與:

await Task.Delay(2000);

回答:

兩者都等待約 2 秒,但等待方式有什麼不同?


練習二|預測執行順序

先不要執行:

Console.WriteLine("1");

Task task =
    WaitAsync();

Console.WriteLine("2");

await task;

Console.WriteLine("3");

async Task WaitAsync()
{
    Console.WriteLine("A");

    await Task.Delay(1000);

    Console.WriteLine("B");
}

預測輸出結果。

答案:

1
A
2
B
3

並回答:

為什麼 A 會在 2 前面?


練習三|理解 Task<T>

看:

async Task<string> GetMessageAsync()
{
    await Task.Delay(1000);

    return "Hello";
}

回答:

GetMessageAsync()
→ 得到什麼?

await GetMessageAsync()
→ 最後得到什麼?

答案:

GetMessageAsync()
→ Task<string>

await GetMessageAsync()
→ string
→ "Hello"

練習四|直接 await 與晚點 await

比較:

await WaitAsync();

Console.WriteLine(
    "其他工作"
);

和:

Task task =
    WaitAsync();

Console.WriteLine(
    "其他工作"
);

await task;

回答:

哪一個版本可以先執行「其他工作」,再等待 WaitAsync() 完成?


Day 22 小結

今天最重要的概念:

概念 用途
Blocking 等待期間阻塞目前 Thread
Task 表示可能尚未完成的操作
Task<T> 表示完成後會得到 T 的操作
async 讓 Method 可以使用 await 撰寫非同步流程
await 等待 Task;尚未完成時暫停目前 Method

最重要的型別關係:

Task<T>
↓
await
↓
T

最重要的執行規則:

呼叫 Async Method
↓
Method 先開始執行
↓
遇到尚未完成的 await
↓
暫停目前 Method 的後續流程
↓
Task 完成
↓
繼續執行

最容易搞混的是:

await
→ 目前 Method 後面的流程要等

但:

await
≠
Thread 一直卡著等

另外:

async / await
≠
自動建立背景 Thread
async / await
≠
讓工作本身自動變快

對有非同步 API 的:

HTTP
Database
File
Network

等 I/O Waiting 工作特別適合使用。

今天最重要的一句話:

await 會暫停目前 Async Method 的後續流程,但不代表把 Thread 一起阻塞;
Task 完成後,再從 await 後面繼續執行。


上一篇
Day 21|Exception Handling:程式發生錯誤時怎麼處理?
下一篇
Day 23|Async 實務:多個非同步工作怎麼處理?
系列文
現在就學C# 與 ASP.NET Core 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言