6.3 — 1. Task và async/await
Task là lời hứa về một kết quả sẽ có trong tương lai, không phải một thread. Đây là điểm hay bị hiểu sai nhất: await một Task không tạo thread nào cả — với I/O thật sự, không có thread nào đang chờ, chỉ có một thông báo được đăng ký với hệ điều hành. Từ khoá async biến phương thức thành một máy trạng thái do trình biên dịch sinh ra, và mỗi await là một điểm mà phương thức có thể tạm dừng rồi tiếp tục. Bài này cũng nói kỹ về async void — thứ duy nhất trong C# mà ngoại lệ bên trong nó làm sập cả tiến trình thay vì đi lên chỗ gọi, và try/catch bao quanh không bắt được.
Mục tiêu bài học
Sau bài này bạn có thể:
- Giải thích
Tasklà gì và vì saoawaitkhông tạo thread. - Mô tả máy trạng thái mà trình biên dịch sinh ra và hai hệ quả của nó.
- Nêu ba vấn đề của
async voidvà biết ngoại lệ duy nhất được phép dùng. - Chọn giữa
TaskvàValueTasktheo tiêu chí rõ ràng. - Biết khi nào bỏ được
async/awaitvà khi nào tuyệt đối không.
Nội dung bài học
6.3.1 — Task là lời hứa, không phải thread
Task task = DoSomethingAsync(); // lời hứa: "sẽ xong"
Task<int> t2 = GetCountAsync(); // lời hứa: "sẽ có một int"
int count = await t2; // chờ lời hứa được thực hiện
Một Task kết thúc ở một trong ba trạng thái: hoàn thành, thất bại (mang theo ngoại lệ), hoặc bị huỷ.
Điểm quan trọng nhất:
awaitkhông tạo thread. Với I/O thật, không có thread nào đang chờ cả.
Khi bạn await một truy vấn database:
- Lệnh được gửi xuống hệ điều hành.
- Phương thức trả về ngay; thread quay lại thread pool và phục vụ việc khác.
- Khi dữ liệu về, hệ điều hành thông báo, và một thread bất kỳ từ pool chạy tiếp phần còn lại.
Không có "thread chờ" nào tồn tại giữa bước 2 và 3. Đó là toàn bộ lý do async giúp được ở bài 6.2.
6.3.2 — async/await là máy trạng thái
public async Task<Order?> GetOrderAsync(int id, CancellationToken ct)
{
var order = await _db.Orders.FindAsync([id], ct); // điểm tạm dừng 1
if (order is null) return null;
order.Lines = await _lines.GetAsync(id, ct); // điểm tạm dừng 2
return order;
}
Trình biên dịch biến phương thức trên thành một struct máy trạng thái với các trạng thái -1, 0, 1. Mỗi await là một điểm máy có thể dừng và quay lại sau.
Hai hệ quả thực tế:
- Biến cục bộ sống qua
awaitđược nâng thành field của máy trạng thái, nên chúng nằm trên heap. Đây chính là chi phí cấp phát của async. - Code sau
awaitcó thể chạy trên thread khác. Đừng giả định thread không đổi, và đừng dùngThreadLocalhay[ThreadStatic]qua ranh giớiawait.