1. Giới thiệu
Trước khi nhảy vào code, hãy hiểu động cơ và tưởng tượng một tình huống điển hình. Giả sử chương trình của bạn đang tải một file từ internet:
// Mã giả
var data = DownloadFile("https://example.com/file");
ProcessData(data);
Vấn đề ở đây là: trong khi tải xuống, chương trình "đứng im". Không có hành động nào khác được thực hiện — người dùng không thể di chuột, không thể nhấn nút, thậm chí chỉ chờ xong mà không gặp một form bị "treo" rất chán.
Trước đây (và trong các ngôn ngữ khác) để giải quyết chuyện này thường phải dùng các Thread, Task, delegates, timers — tất cả dẫn tới mã rối mắt, khó đọc và bảo trì. Ở C# đội ngũ dev quyết định làm cuộc sống dễ hơn: xuất hiện bất đồng bộ với các từ khóa async và await. Giờ có thể viết mã async gần như đơn giản như mã thường.
Cơn "đau" bất đồng bộ cổ điển nếu không có async/await
Để so sánh, xem việc thực hiện thao tác dài với Thread thế nào nếu muốn không chặn giao diện:
// Ví dụ không dùng async/await, thủ công
var thread = new Thread(() =>
{
var data = DownloadFile("https://example.com/file");
Console.WriteLine("Tệp đã tải xong!");
});
thread.Start();
Cách này khá thô sơ: bạn phải tự quản lý thread, không có cách đơn giản để "chờ" kết quả, và xử lý lỗi trở nên khó khăn.
2. Bất đồng bộ trong C#: cú pháp
Định nghĩa: async và await là gì?
async — là một modifier, làm cho method không "nhàm chán" nữa và biến nó thành asynchronous. Method kiểu này thường trả về Task (hoặc Task<T>), hoặc ValueTask. Đây là một lời hứa rằng kết quả sẽ có, nhưng hơi muộn (như đặt hàng online — đặt rồi chờ).
await — là toán tử nói: "Dừng ở dòng này khi đến đây. Chờ cho đến khi operation xong, nhưng đừng chặn luồng! Phần còn lại có thể chạy tiếp."
Async method trông như thế nào?
public async Task MyAsyncMethod()
{
Console.WriteLine("Đang tải file...");
var data = await DownloadFileAsync("https://example.com/file"); // Chờ kết quả bất đồng bộ!
Console.WriteLine("Xong!");
}
Lưu ý:
- Phương thức được thêm modifier async.
- Bên trong dùng await cho thao tác bất đồng bộ.
- Method trả về Task (hoặc Task<T> nếu có giá trị trả về).
Minh họa: chuyện gì diễn ra khi gọi async-method?
graph LR
A[Gọi MyAsyncMethod] --> B[Thực thi tới await]
B --> C[Gọi DownloadFileAsync]
C --> D{Đang chờ}
D --> |File chưa tải| E[Giải phóng luồng]
D --> |File đã tải| F[Thực thi sau await]
F --> G[Kết thúc phương thức]
- Cho tới await đầu tiên phương thức chạy đồng bộ.
- Tại await phương thức bị tạm dừng, điều khiển trả về code gọi.
- Khi operation bất đồng bộ hoàn thành, việc thực thi tiếp tục sau await — như thể chẳng có gì xảy ra.
3. Ví dụ: tải bất đồng bộ dùng async/await
Giả sử ứng dụng học tập của chúng ta tải text từ internet và in độ dài của nó mà không chặn phần còn lại của chương trình.
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
// Phương thức bất đồng bộ (trả về Task)
public static async Task DownloadAndPrintLengthAsync(string url)
{
Console.WriteLine("Bắt đầu tải...");
// Dùng HttpClient — hỗ trợ các phương thức bất đồng bộ
using (var client = new HttpClient())
{
string data = await client.GetStringAsync(url);
Console.WriteLine($"Tải xong! Độ dài văn bản: {data.Length} ký tự.");
}
Console.WriteLine("Phương thức đã hoàn thành.");
}
static void Main()
{
// Khởi chạy thao tác bất đồng bộ và chờ nó hoàn thành
var task = DownloadAndPrintLengthAsync("https://www.example.com");
task.Wait(); // Với ví dụ console đơn giản thì chấp nhận được. Trong UI hoặc web thực tế, lệnh này sẽ chặn và có thể gây deadlock.
}
}
Giải thích:
- DownloadAndPrintLengthAsync — hoàn toàn bất đồng bộ, nhờ async và await.
- Bên trong phương thức chúng ta chờ kết thúc việc tải chuỗi bằng await.
- Trong Main() chúng ta khởi chạy task và đợi rõ ràng bằng Wait(). Trong C# hiện đại bạn có thể làm Main bất đồng bộ (async Task Main) và dùng trực tiếp await.
4. Cơ chế hoạt động
Sự khác nhau giữa mã đồng bộ và bất đồng bộ
PHIÊN BẢN ĐỒNG BỘ
Console.WriteLine("Bắt đầu");
string data = client.GetStringAsync(url).Result; // .Result chặn luồng!
Console.WriteLine("Thao tác hoàn thành");
PHIÊN BẢN BẤT ĐỒNG BỘ
Console.WriteLine("Bắt đầu");
string data = await client.GetStringAsync(url); // Luồng không bị chặn
Console.WriteLine("Thao tác hoàn thành");
Await hoạt động như thế nào "bên trong"?
Khi bạn dùng await, C# tự động "tách" phương thức của bạn thành hai (hoặc nhiều) phần: tất cả những gì trước await và tất cả những gì sau. Khi phương thức bất đồng bộ gọi trả về Task, phương thức của bạn trả về cho code gọi, và khi Task hoàn tất — việc thực thi tiếp tục sau await. Tất cả điều này diễn ra tự động; bạn không phải lo switch giữa các luồng hay quản lý handoff.
Thú vị: C# "biến" phương thức bất đồng bộ của bạn thành một state-machine, nơi mỗi await là một điểm trả về mới.
Sử dụng async/await trong ứng dụng thật
static async Task Main(string[] args)
{
var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
// Trong khi đang tải, làm thứ khác
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Đang làm việc... lần lặp {i}");
await Task.Delay(500); // Tạm dừng 0,5 giây, giả lập công việc
}
await downloadTask; // Chờ tải xong
}
Bây giờ chương trình vừa tải, vừa không bị chặn: "sống" như một con mèo, cùng lúc ngủ và dòm bát ăn.
Bất đồng bộ ≠ đa luồng
Điều quan trọng mà người mới thường nhầm: mã bất đồng bộ có thể chạy trên cùng một luồng! Bất đồng bộ là để không chặn luồng, chứ không phải nhất thiết tạo luồng mới. Luồng hệ điều hành là tài nguyên đắt đỏ! Bất đồng bộ cho phép "thả" luồng chạy tiếp các công việc khác thay vì đợi các thao tác dài (mạng, file, timer, v.v.).
Bảng so sánh: Khi nào chọn gì?
| Kịch bản | Thread/Task | async/await |
|---|---|---|
| Tác vụ CPU-bound | Có | Có (qua Task.Run) |
| Tác vụ I/O-bound | Không hiệu quả | Lý tưởng |
| Nhiều song song | Khó | Dễ |
| Độ đơn giản code | Khó nhằn | Dễ đọc |
5. Các lưu ý hữu ích
Async Main
Với phiên bản C# hiện đại bạn có thể làm Main bất đồng bộ!
static async Task Main(string[] args)
{
// Toàn bộ mã bất đồng bộ của bạn có thể await trực tiếp trong Main
await DownloadAndAnalyzeFileAsync("https://example.com/file");
}
Lỗi điển hình: quên await — task "bốc hơi"
SomeAsyncFunction(); // gọi mà không await, không ai chờ!
Kết quả là task sẽ chạy "trong vô định" — và nếu có lỗi phát sinh bạn thậm chí không biết nó đã xảy ra!
Khi KHÔNG nên viết async-method
- Nếu trong method không có bất kỳ thao tác bất đồng bộ nào (không có await), thì không cần đánh dấu async.
- Không nên viết method là async void (trừ khi bạn là handler sự kiện).
Quy tắc ngắn gọn và câu hỏi thường gặp
- Có thể dùng await ngoài method async không? Không! Luôn chỉ trong method được đánh dấu async.
- Có thể có nhiều await trong một method không? Có, bao nhiêu cũng được — mỗi cái là một "điểm chờ".
- Nếu muốn trả kết quả thì sao? Dùng Task<T> và dùng return.
- Có thể kết hợp nhiều task bất đồng bộ không? Tất nhiên! Có thể khởi chạy nhiều task song song và chờ tất cả bằng Task.WhenAll.
6. Lỗi phổ biến khi dùng async/await
Lỗi #1: "quả cầu lửa" — gọi async mà không await.
DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("Mọi thứ đã xong!"); // Thực ra việc tải vẫn đang diễn ra!
Code SẼ không chờ thao tác bất đồng bộ hoàn tất. Mọi task bất đồng bộ cần được await hoặc rõ ràng chờ bằng Wait() (nhưng cách thứ hai nguy hiểm, dễ gây block).
Lỗi #2: trộn code đồng bộ và bất đồng bộ qua .Result hoặc .Wait().
Đây là anti-pattern, làm mất hết lợi ích của bất đồng bộ. Trong ứng dụng UI hoặc ASP.NET điều này gần như chắc chắn dẫn tới deadlock: task bất đồng bộ chờ luồng được giải phóng, trong khi luồng đang bị block vì chờ task. Ghi nhớ câu thần chú: "async all the way" (bất đồng bộ lên tới đỉnh).
Lỗi #3: Dùng async void.
Method async void không thể được await, và ngoại lệ ném ra trong chúng không thể catch bằng try-catch thông thường, thường gây crash toàn ứng dụng. Chỉ dùng async void cho handler sự kiện (ví dụ async void Button_Click(...)) khi signature yêu cầu. Trong mọi trường hợp khác hãy dùng async Task.
Lỗi #4: Thừa async trong method không có await.
Nếu bạn đánh dấu method là async mà không dùng bên trong await, compiler sẽ cảnh báo. Mã như vậy sẽ chạy hoàn toàn đồng bộ nhưng tốn tài nguyên vì tạo state-machine không cần thiết. Điều này gây hiểu nhầm và giảm hiệu năng.
GO TO FULL VERSION