
Các lỗi nên tránh khi làm việc với async/await trong C#
February 21, 2025Khi sử dụng async và await trong C#, có một số lỗi phổ biến mà anh em khi code thường mắc phải. Những lỗi này có thể dẫn đến các vấn đề về hiệu suất, lỗi logic và khó khăn trong việc debug. Dưới đây là một số lỗi thường gặp và cách khắc phục chúng mà mình gặp phải, anh em có thể góp ý thêm dưới comments nhé!
1. Chặn mã bất đồng bộ (Blocking on Async Code) bằng .Result hoặc .Wait()
Vấn đề:
Chặn mã async bằng cách sử dụng .Result
hoặc .Wait()
có thể dẫn đến deadlock, đặc biệt trong các ứng dụng UI hoặc ASP.NET. Khi một phương thức async bị chặn, nó có thể khiến UI thread (trong WinForms/WPF) hoặc request thread (trong ASP.NET) bị treo vô thời hạn, ngăn không cho các thao tác tiếp theo được thực thi.
Ví dụ sai:
var result = SomeAsyncMethod().Result; // Sai: Chặn luồng chính
Giải pháp:
Luôn sử dụng await
khi gọi các phương thức async.
var result = await SomeAsyncMethod(); // Đúng
Tại sao đây là vấn đề?
- Trong các ứng dụng UI (WinForms, WPF), chặn luồng chính có thể làm giao diện bị treo.
- Trong ASP.NET, chặn luồng có thể gây cạn kiệt tài nguyên của thread pool, làm giảm khả năng mở rộng của ứng dụng.
2. Không sử dụng ConfigureAwait(false) trong codebase
Vấn đề:
Khi viết code thư viện (như utility class), bạn thường không muốn tiếp tục thực thi trên cùng một Synchronization Context (ví dụ: UI thread trong ứng dụng desktop hoặc request thread trong ASP.NET). Theo mặc định, await
sẽ cố gắng tiếp tục trên cùng một context trừ khi được chỉ định khác.
Ví dụ sai:
public async Task Foo()
{
var result = await SomeLongRunningTask(); // Chạy trên context ban đầu
}
Giải pháp:
Sử dụng ConfigureAwait(false)
để tránh việc marshal quay lại context ban đầu trừ khi cần thiết.
public async Task Foo()
{
var result = await SomeLongRunningTask().ConfigureAwait(false);
}
Tại sao đây là vấn đề?
- Giảm số lần chuyển đổi luồng không cần thiết, giúp cải thiện hiệu suất.
- Ngăn chặn deadlock trong các môi trường như ASP.NET hoặc Windows Forms.
3. Lạm dụng async không cần thiết
Vấn đề:
Việc đánh dấu một phương thức là async
khi không có thao tác bất đồng bộ nào thực sự có thể làm tăng độ phức tạp không cần thiết. Nếu phương thức không thực hiện bất kỳ thao tác I/O nào, việc sử dụng async chỉ gây ra chi phí quản lý không cần thiết.
Ví dụ sai:
public async Task<int> CalculateSum()
{
return 1 + 1; // Thao tác đồng bộ nhưng lại dùng async
}
Giải pháp:
Chỉ đánh dấu async nếu phương thức thực sự có thao tác bất đồng bộ.
public int CalculateSum()
{
return 1 + 1;
}
Tại sao đây là vấn đề?
- Gây ra overhead do tạo Task không cần thiết.
- Làm mã nguồn khó đọc và bảo trì hơn.
4. Await bên trong vòng lặp (Awaiting Inside a Loop)
Vấn đề:
Await bên trong vòng lặp có thể dẫn đến việc thực thi tuần tự thay vì song song, làm giảm hiệu suất.
Ví dụ sai:
public async Task ProcessItems()
{
foreach (var item in items)
{
await ProcessItemAsync(item); // Chạy tuần tự
}
}
Giải pháp:
Sử dụng Task.WhenAll()
để thực thi các tác vụ song song khi chúng không phụ thuộc vào nhau.
public async Task ProcessItems()
{
var tasks = items.Select(item => ProcessItemAsync(item));
await Task.WhenAll(tasks); // Chạy đồng thời
}
Tại sao đây là vấn đề?
await
trong vòng lặp khiến các tác vụ chạy tuần tự thay vì đồng thời.Task.WhenAll()
giúp giảm tổng thời gian thực thi.
5. Không xử lý ngoại lệ trong block code async
Vấn đề:
Nếu một ngoại lệ xảy ra trong một phương thức async và không được xử lý đúng cách, nó có thể bị mất hoặc dẫn đến hành vi không mong muốn.
Ví dụ sai:
public async Task DoWork()
{
var result = await Task.Run(() => throw new Exception("Lỗi xảy ra"));
}
Giải pháp:
Sử dụng try-catch
để bắt và xử lý ngoại lệ.
public async Task DoWork()
{
try
{
var result = await Task.Run(() => throw new Exception("Lỗi xảy ra"));
}
catch (Exception ex)
{
// Xử lý ngoại lệ
}
}
Tại sao đây là vấn đề?
- Ngoại lệ không được bắt có thể dẫn đến ứng dụng bị crash.
- Xử lý ngoại lệ giúp đảm bảo mã hoạt động ổn định.
6. Sử dụng async void ngoài event handler
Vấn đề:
Các phương thức async void
thường chỉ nên được sử dụng cho event handlers. Nếu dùng trong các ngữ cảnh khác, chúng không thể được await
, gây khó khăn trong việc xử lý lỗi.
Ví dụ sai:
public async void DoWorkAsync()
{
await Task.Delay(1000);
}
Giải pháp:
Trả về Task
thay vì void
để có thể await.
public async Task DoWorkAsync()
{
await Task.Delay(1000);
}
Tại sao đây là vấn đề?
async void
không thểawait
, làm mất khả năng kiểm soát luồng thực thi.- Ngoại lệ trong
async void
không thể bị bắt bằngtry-catch
từ bên ngoài.
7. Gọi async từ code đồng bộ
Vấn đề:
Nếu gọi một phương thức async từ mã đồng bộ mà không xử lý đúng, có thể dẫn đến mất kết quả hoặc hành vi không mong muốn.
Ví dụ sai:
public void MyMethod()
{
SomeAsyncMethod(); // Quên await
}
Giải pháp:
Đảm bảo rằng phương thức được await
hoặc xử lý đúng cách.
public async Task MyMethod()
{
await SomeAsyncMethod();
}
Bằng cách tránh các lỗi phổ biến này, anh em có thể cải thiện hiệu suất và độ ổn định của mã async trong C#.