Async/Await Best Practices trong C#
Hi anh em, lại là mình đây. Tiếp tục series chia sẻ kinh nghiệm lập trình và các best practice, hôm nay mình sẽ nói về async/await trong C#.
Trong quá trình làm việc, mình thấy việc sử dụng async/await không đúng cách có thể gây ra nhiều vấn đề nghiêm trọng như memory leak, thread pool starvation hay thậm chí crash ứng dụng. Trong bài viết này, chúng ta sẽ tìm hiểu về các best practices khi sử dụng async/await trong C#..
I. Lý thuyết căn bản
1. CPU-bound vs I/O-bound Operations
- CPU-bound operations: Là các tác vụ nặng về tính toán CPU như xử lý logic (if/else), thực hiện các phép tính số học, xử lý chuỗi,...
- I/O-bound operations: Là các tác vụ liên quan đến input/output như đọc/ghi file, gọi network (HTTP, RPC, Database). Khi thực hiện các tác vụ I/O, ứng dụng sẽ yêu cầu HĐH thực hiện và chờ kết quả trả về.
Lập trình bất đồng bộ chủ yếu tập trung vào việc tối ưu các tác vụ I/O-bound, giúp tận dụng tốt hơn các thread trong ứng dụng.
2. ThreadPool
ThreadPool là một tập hợp các thread được CLR (Common Language Runtime) tạo sẵn khi khởi chạy ứng dụng. Các thread trong ThreadPool được gọi là Managed Thread và được quản lý bởi CLR. Việc sử dụng ThreadPool giúp:
- Tối ưu cho các tác vụ I/O hoặc các tác vụ CPU ngắn (< 1s)
- Hạn chế việc tạo và hủy thread thường xuyên, giúp tăng hiệu năng
- Tái sử dụng thread hiệu quả
3. Task vs Thread
Nhiều người thường nhầm lẫn giữa Task và Thread. Task đại diện cho một tác vụ bất đồng bộ và có thể được xử lý trên một hoặc nhiều thread. Task tương tự như concept Promise trong JavaScript - nó đại diện cho một công việc sẽ được hoàn thành trong tương lai.
II. Best Practices
1. Không sử dụng async void
Async void chỉ nên được sử dụng cho event handlers. Trong ASP.NET, async void có thể gây crash ứng dụng vì exception trong method async void sẽ không được xử lý, kể cả khi đã try-catch.
// BAD
public async void HandleEvent()
{
await Task.Delay(1000);
throw new Exception("Error"); // Exception sẽ không được bắt
}
// GOOD
public async Task HandleEvent()
{
await Task.Delay(1000);
throw new Exception("Error"); // Exception có thể được bắt
}
2. Sử dụng async mọi nơi có thể
Đảm bảo sử dụng async/await với mọi tác vụ I/O và trong toàn bộ call stack.
// BAD
public async Task<string> GetDataAsync()
{
var result = GetDataFromDbSync(); // Blocking call
return result;
}
// GOOD
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDbAsync(); // Non-blocking call
return result;
}
3. Sử dụng Task.FromResult thay vì Task.Run cho các tính toán đơn giản
// BAD
public Task<int> GetValueAsync()
{
return Task.Run(() => 42);
}
// GOOD
public Task<int> GetValueAsync()
{
return Task.FromResult(42);
}
4. Tránh blocking calls
Không sử dụng Task.Wait(), Task.Result, hay Task.GetAwaiter().GetResult(). Các lệnh này sẽ block thread và có thể gây deadlock.
// BAD
public void DoWork()
{
var task = LongRunningTaskAsync();
var result = task.Result; // Blocking call
}
// GOOD
public async Task DoWorkAsync()
{
var result = await LongRunningTaskAsync();
}
5. Sử dụng CancellationToken
public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
// Process data
}
}
6. Start task sớm và await khi cần
// BAD
public async Task<string> GetDataAsync()
{
var data1 = await GetFirstDataAsync();
var data2 = await GetSecondDataAsync();
return $"{data1}-{data2}";
}
// GOOD
public async Task<string> GetDataAsync()
{
var task1 = GetFirstDataAsync(); // Start first task
var task2 = GetSecondDataAsync(); // Start second task
var data1 = await task1; // Await when needed
var data2 = await task2;
return $"{data1}-{data2}";
}
7. Sử dụng ConfigureAwait(false) khi viết SDK
public async Task<string> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return "Result";
}
8. Luôn gọi FlushAsync() với Stream
// GOOD
await using (var writer = new StreamWriter("file.txt"))
{
await writer.WriteAsync("data");
await writer.FlushAsync();
}
III. Kết luận
Việc sử dụng async/await đúng cách sẽ giúp tăng hiệu năng và độ ổn định của ứng dụng. Tuy nhiên, cần phải hiểu rõ các concepts cơ bản và tuân thủ các best practices để tránh các vấn đề tiềm ẩn.
Hy vọng bài viết này sẽ giúp anh em hiểu rõ hơn về async/await trong C# và áp dụng hiệu quả trong dự án của mình. Nếu anh em quan tâm đến những best practice về lập trình có thể theo dõi mình tại:
- Youtube: https://www.youtube.com/@VietAnhDang47
- TikTok: https://www.tiktok.com/@vietanhdangcoder
- LinkedIn: https://www.linkedin.com/in/vietanh47/
Tham khảo
All rights reserved