Cho mình hỏi về C# thread safe
Mình có search gg, thì có bảo rằng dùng câu lệnh: Parallel.ForEach vẫn không đảm bảo được thread safe nếu nhiều thread cùng đọc / ghi 1 data toàn cục. Và vậy mình thử làm 1 ví dụ như sau:
class A
{
public int a { get; set; } = 2;
public int Method(int input)
{
a += 2; // ghi - tăng giá trị của property a
return input + 2;
}
}
static void Main(string[] args)
{
var singetonObj = new A(); // obj toàn cục
var listThread = new List<int> { 6, 2, 8, 3, 7 }; // các thread
var listResult = new List<int>();
Parallel.ForEach(lstThread, t =>
{
lstResult.Add(singetonObj.Method(singetonObj.a + t)); // đọc - lấy giá trị của property a
});
}
Nếu theo như trên mạng nói, thì việc đọc / ghi biến a như vậy sẽ dẫn đến văng exception về thread safe, nhưng không. Kết quả là vẫn chạy bình thường, và giá trị cuối cùng của biến a == 12. Kể cả mình đổi property của a là static luôn vẫn chạy bình thường, và kết quả a == 12. Vậy tóm lại Parallel.ForEach có thread safe hay ko ?
1 CÂU TRẢ LỜI
Chào bạn, một câu hỏi rất hay và là "bẫy" kinh điển mà hầu hết mọi người đều gặp phải khi mới làm quen với lập trình đa luồng (Multi-threading).Để trả lời thẳng vào vấn đề: Parallel.ForEach KHÔNG hề tự động đảm bảo thread-safe cho các dữ liệu bên ngoài mà nó tác động vào.Tại sao code của bạn vẫn chạy "bình thường" và ra kết quả 12? Hãy cùng mổ xẻ từng chút một nhé.1. Sự khác biệt giữa "Chạy được" và "Thread-safe"Trong lập trình đa luồng, việc không có Exception không có nghĩa là code của bạn đúng.Tại sao bạn thấy kết quả vẫn là 12?Dữ liệu quá nhỏ: Bạn chỉ có 5 phần tử. Tốc độ CPU hiện nay quá nhanh, có thể các thread thực thi xong xuôi trước khi kịp tranh chấp nhau, hoặc cơ chế lập lịch (Task Scheduler) chạy chúng gần như tuần tự.Race Condition (Tranh chấp dữ liệu): Phép toán a += 2 thực chất gồm 3 bước ở tầng CPU:Bước 1: Đọc giá trị a từ bộ nhớ vào thanh ghi.Bước 2: Cộng 2 vào giá trị đó.Bước 3: Ghi giá trị mới ngược lại bộ nhớ.Nếu 2 thread cùng chạy Bước 1 cùng lúc (giả sử ), cả hai đều thấy là 2. Sau đó cả hai cùng cộng thành 4 và ghi đè vào . Kết quả cuối cùng thay vì . Đây gọi là mất mát dữ liệu (lost update).2. Những "quả bom nổ chậm" trong ví dụ của bạnĐoạn code của bạn thực tế có tới 2 lỗi nghiêm trọng về thread-safety:Lỗi 1: a += 2 (Shared State)Như đã phân tích ở trên, đây là lỗi Race Condition. Nếu bạn tăng listThread lên 1,000,000 phần tử, chắc chắn kết quả cuối cùng của a sẽ nhỏ hơn con số kỳ vọng. Nó không văng lỗi, nó chỉ ra kết quả sai.Lỗi 2: listResult.Add(...) (Collection Safety)Đây mới là chỗ dễ văng Exception nhất. List<T> trong C# không thread-safe.Khi Add, nếu hai thread cùng nhảy vào một lúc, chúng có thể ghi đè lên cùng một chỉ số (index) trong mảng nội bộ của List.Nguy hiểm hơn, khi List đầy và cần "resize" (tăng kích thước mảng), một thread đang resize trong khi thread kia đang ghi sẽ dẫn đến lỗi IndexOutOfRangeException hoặc NullReferenceException.Tại sao bạn chưa thấy lỗi? Vì 5 phần tử là quá ít để List phải resize hay gây xung đột bộ nhớ đáng kể.3. Cách viết đúng (Thread-safe)Để xử lý bài toán này, bạn có 3 hướng chính:Cách 1: Dùng lock (Đơn giản nhất)Khóa đối tượng lại để tại một thời điểm chỉ có 1 thread được quyền truy cập.
private readonly object _lockObj = new object(); // ... bên trong Parallel.ForEach lock(_lockObj) { singetonObj.Method(...); listResult.Add(...); }
Cách 2: Dùng Interlocked (Hiệu năng cao cho số nguyên) Dành riêng cho việc cộng/trừ các biến số mà không cần dùng lock.
// Thay vì a += 2, trong Method bạn dùng: Interlocked.Add(ref _a, 2);
Cách 3: Dùng Thread-safe Collections Thay vì List<int>, hãy dùng ConcurrentBag<int>, ConcurrentQueue<int>, hoặc ConcurrentStack<int>.
var listResult = new ConcurrentBag<int>(); // Thêm an toàn từ nhiều luồng
Parallel.ForEach có thread-safe không? KHÔNG. Nó chỉ giúp bạn chia việc ra nhiều luồng thôi.
Tại sao không lỗi? Do dữ liệu nhỏ và may mắn. Trong hệ thống thực tế, nó sẽ tạo ra những lỗi "lúc hiện lúc ẩn" cực kỳ khó debug.
Lời khuyên: Luôn giả định rằng bất cứ biến toàn cục nào được đọc/ghi bởi nhiều thread đều không an toàn cho đến khi bạn dùng các cơ chế đồng bộ hóa (lock, Interlocked, Concurrent collections).