Đây là chủ đề bị hiểu sai nhiều nhất ở level senior. Nhiều người "dùng async hàng ngày", nhưng thực ra chỉ đang viết code chạy được, chưa hẳn hiểu cách runtime vận hành.
Mục tiêu
- Hiểu bản chất async/await
- Phân biệt async với multithreading
- Biết khi nào nên và không nên dùng
Task.Run - Tránh deadlock, thread starvation, fake async
- Review async code như một master
1) Bản chất thật sự của async/await
Điều quan trọng số 1:
async/awaitkhông phải là "chạy trên thread khác". Nó là cơ chế giúp bạn viết asynchronous non-blocking flow dưới dạng code tuần tự dễ đọc.
Khi bạn viết:
csharpvar data = await httpClient.GetStringAsync("https://api.com/users");
Console.WriteLine(data);
Chuyện gì thực sự xảy ra:
GetStringAsyncgửi HTTP request → không block thread- Thread hiện tại được trả về thread pool (hoặc trả lại UI thread)
- Khi response về, runtime lấy một thread bất kỳ từ pool (hoặc quay lại UI thread nếu có
SynchronizationContext) để chạy tiếp phần sauawait
Thread A: ──[gửi request]──→ trả thread về pool ──→ (rảnh, làm việc khác)
⬇
(response về)
⬇
Thread B: ──────────────────────────→ [chạy Console.WriteLine]
Kết luận: async giúp giải phóng thread trong lúc chờ I/O, KHÔNG phải tạo thread mới.
2) Async ≠ Multi-threading
Đây là nhầm lẫn phổ biến nhất:
| Async/Await | Multi-threading | |
|---|---|---|
| Bản chất | Giải phóng thread khi chờ I/O | Chạy code song song trên nhiều threads |
| Mục đích | Tránh block thread (scalability) | Tận dụng nhiều CPU cores (parallelism) |
| Khi nào dùng | I/O: HTTP, database, file, network | CPU-bound: tính toán nặng, image processing |
| Thread mới? | Không (trả thread về pool) | Có (dùng thread pool hoặc tạo mới) |
Ví dụ minh họa rõ nhất
csharp// Async I/O - KHÔNG dùng thread mới
async Task<string> GetUserAsync()
{
// Thread được trả về pool trong lúc chờ DB
var user = await db.Users.FirstOrDefaultAsync(u => u.Id == 1);
return user.Name;
}
// Multi-threading - DÙNG thread từ pool
Task<int> CalculateHeavyAsync()
{
// Đẩy computation sang thread pool
return Task.Run(() =>
{
// Chạy trên thread khác, CPU goes brrrr
return Fibonacci(1_000_000);
});
}
Câu hỏi tự kiểm tra
Bạn đang gọi API bên thứ 3, chờ response mất 2 giây. Trong lúc chờ, thread đang làm gì?
Nếu dùng async đúng: Thread rảnh, được trả về pool, phục vụ request khác.
Nếu dùng sync (.Result): Thread bị block, nằm chờ, không làm gì → thread starvation nếu load cao.
3) SynchronizationContext - "Thủ phạm" của deadlock
Vấn đề kinh điển
csharp// ❌ DEADLOCK trong ASP.NET Framework (pre-.NET Core) hoặc WPF/WinForms
public string GetUserSync()
{
// .Result block thread hiện tại (UI thread hoặc request thread)
var user = GetUserAsync().Result; // 💀 DEADLOCK
return user.Name;
}
async Task<User> GetUserAsync()
{
var user = await db.Users.FindAsync(1);
// → await muốn quay lại thread gốc (SynchronizationContext)
// → nhưng thread gốc đang bị block bởi .Result
// → DEADLOCK!
return user;
}
Tại sao deadlock?
1. GetUserSync() chạy trên Thread A
2. Gọi GetUserAsync().Result → Block Thread A, chờ Task hoàn thành
3. Trong GetUserAsync(), await db.Users.FindAsync(1) hoàn thành
4. SynchronizationContext nói: "phải quay lại Thread A để chạy tiếp"
5. Nhưng Thread A đang bị block bởi .Result → 💀 DEADLOCK
Giải pháp
csharp// ✅ Giải pháp 1: Dùng async all the way (BẮT BUỘC)
public async Task<string> GetUserAsync()
{
var user = await GetUserFromDbAsync();
return user.Name;
}
// ✅ Giải pháp 2: ConfigureAwait(false) trong library code
async Task<User> GetUserFromDbAsync()
{
var user = await db.Users.FindAsync(1).ConfigureAwait(false);
// → Nói với runtime: KHÔNG CẦN quay lại thread/context gốc
// → Chạy tiếp trên bất kỳ thread nào từ pool
return user;
}
ConfigureAwait - Khi nào dùng?
| Code type | ConfigureAwait(false) | Lý do |
|---|---|---|
| Library/NuGet package | ✅ LUÔN dùng | Không biết consumer có SynchronizationContext không |
| ASP.NET Core app | 🔄 Không bắt buộc | ASP.NET Core KHÔNG có SynchronizationContext |
| WPF/WinForms/MAUI | ⚠️ Cẩn thận | Nếu dùng → code sau await KHÔNG chạy trên UI thread |
| Blazor Server | ✅ Nên dùng | Giống library code |
ASP.NET Core không có SynchronizationContext, nên deadlock kiểu trên KHÔNG xảy ra. Nhưng
.Resultvẫn block thread → thread starvation vẫn là vấn đề!
4) Thread Starvation - Khi Nào Xảy Ra?
csharp// Thread pool mặc định: ~(số CPU cores) threads ban đầu
// Ví dụ: server 4 cores → bắt đầu với ~4 threads
// ❌ Block 100 threads cùng lúc
app.MapGet("/users/{id}", (int id) =>
{
// .Result block thread, thread pool phải tạo thêm threads
// Thread injection rate: ~1-2 threads/giây (RẤT CHẬM!)
var user = httpClient.GetStringAsync($"/api/users/{id}").Result;
return user;
});
// 100 requests đồng thời → 96 requests phải chờ thread → timeout!
// ✅ Async: 100 requests, vẫn chỉ cần vài threads
app.MapGet("/users/{id}", async (int id) =>
{
var user = await httpClient.GetStringAsync($"/api/users/{id}");
return user;
});
// 100 requests → mỗi request gửi HTTP rồi trả thread → 4 threads đủ phục vụ!
Dấu hiệu nhận biết Thread Starvation
1. Response time tăng đột ngột dưới load vừa phải
2. dotnet-counters: "ThreadPool Thread Count" tăng liên tục
3. Nhiều thread ở trạng thái "waiting" trong dump
4. App "lag" nhưng CPU thấp (threads bị block, không CPU-bound)
5) Task.Run - Khi Nào Dùng, Khi Nào KHÔNG
❌ Sai: Wrap async method trong Task.Run
csharp// ❌ KHÔNG wrap I/O async trong Task.Run
// Task.Run lấy thread từ pool → async method trả thread về → vô nghĩa!
var data = await Task.Run(() => httpClient.GetStringAsync(url));
// → Dùng 1 thread để... giải phóng 1 thread? Tốn overhead, không gain gì.
// ✅ Gọi async method trực tiếp
var data = await httpClient.GetStringAsync(url);
✅ Đúng: CPU-bound work
csharp// ✅ Task.Run cho CPU-bound work
// Offload computation nặng khỏi request thread
app.MapGet("/report", async () =>
{
// Computation mất 5 giây → block request thread nếu không dùng Task.Run
var report = await Task.Run(() => GenerateComplexReport());
return report;
});
int Fibonacci(int n)
{
// Pure CPU work, không có I/O
if (n <= 1) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
⚠️ Cẩn thận: Task.Run trong ASP.NET
csharp// ⚠️ Trong ASP.NET, Task.Run có thể gây HẠI
// Vì: request thread đã ở trong thread pool rồi!
// Task.Run = lấy THÊM 1 thread từ pool → giảm throughput
// Chỉ dùng Task.Run trong ASP.NET khi:
// 1. CPU-bound work sẽ block request thread quá lâu (>50ms)
// 2. Bạn muốn nó chạy parallel với work khác
// Trong Desktop app (WPF/WinForms): Task.Run HỢP LÝ hơn
// Vì: offload khỏi UI thread để UI không freeze
await Task.Run(() => HeavyComputation()); // UI thread rảnh → app responsive
6) Fake Async - Anti-Pattern Nguy Hiểm
csharp// ❌ FAKE ASYNC: method async nhưng bên trong sync
// Cho caller ẢO TƯỞNG là async, thực tế block thread!
async Task<User> GetUserAsync(int id)
{
// DbContext không có sync API nên dev... wrap sync code
return await Task.Run(() =>
{
using var conn = new SqlConnection(connStr);
conn.Open(); // 💀 Block thread!
var cmd = new SqlCommand("SELECT * FROM Users WHERE Id = @id", conn);
cmd.Parameters.AddWithValue("@id", id);
// ...đọc data synchronously...
});
}
// ✅ REAL ASYNC: dùng native async APIs
async Task<User> GetUserAsync(int id)
{
await using var conn = new SqlConnection(connStr);
await conn.OpenAsync(); // ✅ Non-blocking
var cmd = new SqlCommand("SELECT * FROM Users WHERE Id = @id", conn);
cmd.Parameters.AddWithValue("@id", id);
await using var reader = await cmd.ExecuteReaderAsync(); // ✅ Non-blocking
// ...
}
Quy tắc vàng: Nếu thư viện có phương thức async (ví dụ OpenAsync, ReadAsync, SaveChangesAsync), luôn dùng bản async. Nếu không có → đó có thể là thư viện cũ, cân nhắc thay thế.
7) Parallel vs Concurrent vs Async
Concurrent (đồng thời):
Nhiều task TIẾN TRIỂN cùng lúc, nhưng có thể KHÔNG chạy song song.
Ví dụ: 1 thread xử lý xen kẽ 3 requests (async I/O)
→ Thread switch giữa các tasks khi 1 task đang chờ I/O
Parallel (song song):
Nhiều tasks chạy CÙ LÚC trên nhiều threads/cores.
Ví dụ: 4 threads tính Fibonacci trên 4 cores
→ Đúng nghĩa song song, cần nhiều CPU cores
Async:
Cơ chế VIẾT CODE cho concurrent flow.
Không tự nó song song, không tự tạo thread.
Task.WhenAll - Concurrent, không nhất thiết Parallel
csharp// 3 HTTP calls chạy CONCURRENT (đồng thời, cùng tiến triển)
// Nhưng có thể chỉ dùng 1-2 threads!
var userTask = httpClient.GetAsync("/users/1");
var orderTask = httpClient.GetAsync("/orders/latest");
var productTask = httpClient.GetAsync("/products/top");
await Task.WhenAll(userTask, orderTask, productTask);
// → 3 requests gửi đi cùng lúc
// → Thread được trả về pool trong lúc chờ
// → Tổng thời gian ≈ max(3 requests), không phải sum(3 requests)
Parallel.ForEachAsync - True Parallel
csharp// .NET 6+: xử lý song song N items
var urls = GetUrlsToProcess(); // 1000 URLs
await Parallel.ForEachAsync(urls, new ParallelOptions
{
MaxDegreeOfParallelism = 10 // Tối đa 10 concurrent
}, async (url, ct) =>
{
var response = await httpClient.GetAsync(url, ct);
await ProcessResponseAsync(response);
});
8) ValueTask - Khi Nào Dùng?
csharp// Task: allocate object trên heap MỖI LẦN gọi
// ValueTask: là struct, KHÔNG allocate nếu result đã sẵn
// ✅ Dùng ValueTask khi: result thường xuyên có sẵn (cached)
async ValueTask<User?> GetUserAsync(int id)
{
// Cache hit → trả về ngay, KHÔNG allocate Task object
if (_cache.TryGetValue(id, out var user))
return user;
// Cache miss → thực sự async
user = await _db.Users.FindAsync(id);
_cache.Set(id, user);
return user;
}
// ❌ KHÔNG dùng ValueTask khi:
// - Method luôn async (luôn await I/O)
// - Caller cần await nhiều lần (ValueTask chỉ await 1 lần!)
// - Interface public rộng (Task dễ compose hơn)
Master Checklist ✅
Khi review async code, hãy kiểm tra:
□ Async all the way? (không .Result, .Wait(), .GetAwaiter().GetResult())
□ Task.Run chỉ dùng cho CPU-bound? (không wrap I/O async)
□ Không fake async? (dùng native async APIs: OpenAsync, ReadAsync...)
□ ConfigureAwait(false) trong library code?
□ Task.WhenAll cho independent async operations?
□ CancellationToken được forward ở mọi tầng?
□ Không async void? (trừ event handlers)
□ Không try/catch nuốt exception trong async?
Common Mistakes Recap
| Mistake | Hậu quả | Fix |
|---|---|---|
.Result / .Wait() | Deadlock hoặc thread starvation | await |
Task.Run(() => AsyncMethod()) | Waste thread, no gain | await AsyncMethod() |
async void | Exception không bắt được, crash app | async Task |
Quên CancellationToken | Request bị cancel nhưng server vẫn xử lý | Forward token |
await trong loop tuần tự | Chậm (sequential) | Task.WhenAll nếu independent |
Tư duy master: Mỗi khi viết
await, hãy nghĩ: "Thread này đi đâu? Khi quay lại, nó ở thread nào?" - Nếu trả lời được, bạn đã hiểu async.