🌟 async và await trong C# - Làm sao để tránh Race Condition
6 phút đọc

🌟 async & await trong C# - Tránh Race Condition thế nào? 🤔
Để hiểu thêm về async/await trong C#, bạn có thể đọc bài viết sau: Async và await trong C#
Một điểm quan trọng khi làm việc với async/await là tránh race condition để đảm bảo dữ liệu không bị xung đột khi có nhiều luồng truy cập cùng lúc.
Vậy race condition là gì? Vấn đề nó gây ra và làm sao để tránh khi làm việc với async/await? Hãy cùng mình tìm hiểu nhé.
🔍 Race Condition là gì?
Race condition xảy ra khi có multiple threads hoặc tasks cùng đọc/ghi dữ liệu, nhưng không có cơ chế kiểm soát hợp lý, dẫn đến kết quả không mong muốn.
👉 Chúng ta cùng xem xét một số ví dụ dễ gây ra race condition sau và cách giải quyết:
❌ Ví dụ 1: Race condition xảy ra khi cập nhật biến toàn cục
Giả sử có một API backend cho phép nhiều request cập nhật bộ đếm (_counter) - một biến dùng chung, giá trị của biến đó có thể không chính xác:
private int _counter = 0;
public async Task IncrementCounterAsync()
{
await Task.Delay(100); // Giả lập xử lý bất đồng bộ
_counter++; // Có thể xảy ra race condition nếu nhiều request chạy đồng thời
}
🔴 Vấn đề: Nếu nhiều request gọi IncrementCounterAsync() cùng lúc, _counter có thể không được cập nhật đúng do nhiều tác vụ đang ghi đè lên giá trị của nhau.
❌ Ví dụ 2: Race condition khi khi file Log
Nếu nhiều tác vụ cùng ghi vào một file log, dữ liệu có thể bị ghi sai hoặc mất một phần.
private async Task LogMessageAsync(string message)
{
using (var writer = new StreamWriter("log.txt", append: true))
{
await writer.WriteLineAsync(message);
}
}
🔴 Vấn đề: Nếu nhiều request cùng ghi vào log.txt, có thể xảy ra lỗi hoặc mất dữ liệu.
❌ Ví dụ 3: Race condition khi xử lý đơn hàng
Giả sử một hệ thống bán hàng cần kiểm tra số lượng tồn kho trước khi xác nhận đơn hàng.
Ví dụ sai:
private int _stock = 5;
public async Task<bool> ProcessOrderAsync()
{
if (_stock > 0)
{
await Task.Delay(100); // Giả lập thời gian xử lý đơn hàng
_stock--; // Nếu có nhiều request cùng lúc, có thể bị giảm quá mức
return true;
}
return false;
}
🔴 Vấn đề: Nếu có hai người mua hàng cùng lúc, cả hai có thể đều thấy _stock > 0 và đơn hàng được xử lý hai lần, dẫn đến tồn kho bị âm.
👉Trong cả 3 ví dụ dưới, chúng ta đều nhật thấy khi có nhiều tasks bất đồng bộ cùng đọc/ghi một tài nguyên dùng chung, dẫn đến dữ liệu bị sai lệch hoặc hành vi không mong muốn của hệ thống.
🛠 Cách ngăn chặn Race condition
✅1. Dùng SemaphoreSlim để kiểm soát truy cập
📌 Với ví dụ 1, SemaphoreSlim giúp đảm bảo chỉ có một task có thể thay đổi biến _counter tại một thời điểm.
private int _counter = 0;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public async Task IncrementCounterAsync()
{
await _semaphore.WaitAsync();
try
{
await Task.Delay(100);
_counter++;
}
finally
{
_semaphore.Release();
}
}
🔹 Lợi ích: Tránh tình trạng nhiều tác vụ cùng thay đổi tài nguyên dùng chung, giúp dữ liệu luôn chính xác.
📌Với ví dụ 2, sử dụng SemaphoreSlim để đảm bảo chỉ một tác vụ ghi log tại một thời điểm.
private readonly SemaphoreSlim _logLock = new SemaphoreSlim(1, 1);
private async Task LogMessageAsync(string message)
{
await _logLock.WaitAsync();
try
{
using (var writer = new StreamWriter("log.txt", append: true))
{
await writer.WriteLineAsync(message);
}
}
finally
{
_logLock.Release();
}
}
🔹 Lợi ích: Đảm bảo dữ liệu log không bị ghi đè khi nhiều request đồng thời.
📌Với ví dụ 3, dùng SemaphoreSlim (Tương thích với async/await)
private int _stock = 5;
private readonly SemaphoreSlim _stockLock = new SemaphoreSlim(1, 1);
public async Task<bool> ProcessOrderAsync()
{
await _stockLock.WaitAsync();
try
{
if (_stock > 0)
{
await Task.Delay(100); // Giả lập xử lý đơn hàng
_stock--;
return true;
}
return false;
}
finally
{
_stockLock.Release();
}
}
🔹 Lợi ích: Hỗ trợ async/await mà vẫn đảm bảo tránh race condition.
⚡Ngoài sử dụng SemaphoreSlim, chúng ta có một số phương pháp để chặn Race Condition. Hãy cùng xem tiếp nhé.
✅ 2. Dùng Interlocked cho biến số nguyên
Nếu chỉ cần cập nhật một biến kiểu số nguyên (int), Interlocked giúp cập nhật an toàn mà không cần lock. Cùng xem xét với ví dụ 1, có thể xử lý như sau:
private int _counter = 0;
public async Task IncrementCounterAsync()
{
await Task.Delay(100);
Interlocked.Increment(ref _counter);
}
🔹 Lợi ích: Đơn giản, hiệu suất cao, không cần dùng semaphore.
✅ 3. Dùng Concurrent Collections cho danh sách dữ liệu
Thay vì List<T>, Dictionary<T, T> hãy sử dụng ConcurrentDictionary hoặc ConcurrentBag.
private ConcurrentDictionary<int, string> _data = new ConcurrentDictionary<int, string>();
public async Task AddDataAsync(int key, string value)
{
await Task.Delay(100);
_data.TryAdd(key, value); // Không gây lỗi nếu nhiều luồng ghi cùng lúc
}
🔹 Lợi ích: Đảm bảo an toàn khi nhiều tác vụ truy cập đồng thời.
🚨 4. Không dùng lock trong Async code (thay bằng SemaphoreSlim)
Sử dụng lock trong async có thể gây deadlock. Chúng ta cùng xem xét Ví dụ 3 bên trên - ví dụ về race condition khi xử lý đơn hàng.
❌ Ví dụ sai (Có thể gây deadlock):
private int _stock = 5;
private readonly object _lock = new object();
public async Task<bool> ProcessOrderAsync()
{
lock (_lock)
{
if (_stock > 0)
{
await Task.Delay(100); // Sai: `lock` không nên dùng trong async
_stock--;
return true;
}
return false;
}
}
✅ Thay thế bằng SemaphoreSlim như phương pháp bên trên mình đã đề cập:
private int _stock = 5;
private readonly SemaphoreSlim _stockLock = new SemaphoreSlim(1, 1);
public async Task<bool> ProcessOrderAsync()
{
await _stockLock.WaitAsync();
try
{
if (_stock > 0)
{
await Task.Delay(100); // Giả lập xử lý đơn hàng
_stock--;
return true;
}
return false;
}
finally
{
_stockLock.Release();
}
}
Nếu bạn muốn dùng lock, thì chỉ dùng cho synchronous code. Khi đó, có thể dùng lock như sau:
private int _stock = 5;
private readonly object _lock = new object();
public bool ProcessOrder()
{
lock (_lock)
{
if (_stock > 0)
{
_stock--;
return true;
}
return false;
}
}
🔹 Lợi ích: Đảm bảo không có hai luồng cùng lúc kiểm tra _stock.
🚨 Lưu ý: lock không dùng được với async/await.
🎯 Kết Luận
- Dùng SemaphoreSlim để khi cần bảo vệ tài nguyên quan trọng như file, database, biến dùng chung.
- Dùng Interlocked nếu chỉ cần tăng/giảm giá trị số nguyên.
- Dùng ConcurrentCollections nếu xử lý dữ liệu List hoặc Dictionary.
- Không dùng lock trong async để tránh deadlock.
⚡Hy vọng những phương pháp trên có thể giúp bạn viết code C# an toàn, hiệu suất cao và tránh lỗi race condition. 🚀
Nguồn: Minh Phương Dương