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 完成
↓
繼續執行
先看:
Console.WriteLine("A");
Thread.Sleep(3000);
Console.WriteLine("B");
執行流程:
A
↓
Thread.Sleep(3000)
↓
Thread 停住約 3 秒
↓
B
Thread.Sleep() 會阻塞目前 Thread。
這稱為:
Blocking
→ 阻塞
也就是:
程式要等
+
Thread 也一起等
改成:
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
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 尚未完成時偷偷先執行。
這裡要分清楚兩件事:
Method 暫停
和:
Thread 被阻塞
不是同一件事。
看:
Console.WriteLine("A");
await Task.Delay(3000);
Console.WriteLine("B");
這裡確實是:
目前 Method
↓
停在 await
↓
B 要等 Task 完成後才能執行
但不代表:
目前 Thread
↓
也要一起卡 3 秒
所以:
Method 暫停
≠
Thread 阻塞
最重要的一句:
await會暫停目前 Method 的後續流程,但不等於同步阻塞 Thread。
Task 是什麼?現在再來看:
Task
Task 可以先理解成:
一個可能還沒有完成的操作。
例如:
Task task =
Task.Delay(3000);
這裡的:
task
代表的是:
「等待 3 秒」這個操作
它可能:
還在進行
也可能:
已經完成
所以:
Task
→ 代表一個操作
而不是:
Task
→ 一條新的 Thread
這兩個概念不要混在一起。
先不要急著看 Task<T>。
先看一般 Method:
string GetMessage()
{
return "Hello";
}
呼叫:
string message =
GetMessage();
流程很直接:
呼叫 GetMessage()
↓
Method 執行完成
↓
得到 "Hello"
所以:
GetMessage()
得到的是:
string
現在假設:
取得訊息需要等待 1 秒。
可以寫成:
async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello";
}
這次呼叫:
GetMessageAsync();
不能立刻得到:
"Hello"
因為操作還沒有完成。
所以 C# 先給我們:
Task<string>
可以先理解成:
一個「完成之後會得到 string」的操作。
也就是:
現在
↓
Task<string>
完成後
↓
string
↓
"Hello"
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
async 是什麼?看:
async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello";
}
前面的:
async
可以先理解成:
讓這個 Method 可以使用
await撰寫非同步流程。
但不要理解成:
加 async
→ 自動建立背景 Thread
也不要理解成:
加 async
→ 程式自動變快
async 本身不會讓工作自動加速。
await 是什麼?await
就是:
等待一個 Task 完成。
例如:
await Task.Delay(1000);
如果 Task 尚未完成:
目前 Method 暫停
↓
等待 Task 完成
↓
Task 完成
↓
繼續執行
如果 Task 本來就已經完成:
await
↓
直接繼續
所以:
只有當 Task 尚未完成時,
await才需要暫停目前 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
剛才的範例非常重要。
這行:
Task task =
WaitAsync();
不是:
WaitAsync()
↓
整個丟到背景
↓
立刻執行下一行
而是:
呼叫 WaitAsync()
↓
WaitAsync 先開始執行
↓
輸出 A
↓
遇到尚未完成的 await
↓
WaitAsync 暫停
↓
呼叫端繼續
所以:
Async Method 會先正常執行,直到遇到尚未完成的
await,才暫停後續流程。
await 和晚點 await 有什麼不同?這個差異非常重要。
awaitawait WaitAsync();
Console.WriteLine(
"其他工作"
);
流程:
WaitAsync()
↓
等待完成
↓
其他工作
也就是:
後面的程式要等
WaitAsync()完成。
Task task =
WaitAsync();
Console.WriteLine(
"其他工作"
);
await task;
流程:
開始 WaitAsync()
↓
取得 Task
↓
先執行其他工作
↓
await task
↓
如果還沒完成就等待
所以:
開始一個非同步操作,和等待它完成,不一定要發生在同一個時間點。
這個觀念之後學:
Task.WhenAll()
會非常重要。
如果有 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。
前面使用:
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 完成
↓
繼續執行
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 陪它一起等。
async / await?最典型的是:
I/O-bound
→ I/O 等待型工作
例如:
HTTP API
Database
File
Network
常見:
await httpClient.GetAsync(...);
await dbContext.SaveChangesAsync();
await query.ToListAsync();
await stream.ReadAsync(...);
這些工作的共同特色:
開始工作
↓
大量時間在等待外部資源
↓
取得結果
例如:
int result =
10 + 20;
或:
decimal total =
orders.Sum(
order => order.Price
);
通常直接執行即可。
可以先記:
有非同步 API 的 I/O 等待
→ 適合 async / await
一般 CPU 計算
→ 通常直接執行
不要因為學了 async,就把所有 Method 都改成 Async Method。
async / await 不代表什麼?不要把:
async / await
理解成:自動建立背景 Thread
也不要理解成:工作本身自動變快
假設 API 原本需要:2 秒
改成:await GetDataAsync();
API 還是可能需要:2 秒
真正改善的是:等待方式
所以:
async / await
≠
自動多執行緒
async / await
≠
自動加速工作
async / await
→
更有效率地處理等待
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。
今天可以整理成:
呼叫 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 WaitAsync();
Console.WriteLine(
"其他工作"
);
和:
Task task =
WaitAsync();
Console.WriteLine(
"其他工作"
);
await task;
回答:
哪一個版本可以先執行「其他工作」,再等待
WaitAsync()完成?
今天最重要的概念:
| 概念 | 用途 |
|---|---|
| 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後面繼續執行。