Phân tích lỗi đồng bộ hóa giữa AutoResetEvent và biến chia sẻ trong C#

Trong quá trình phát triển các hệ thống xử lý song song, việc đảm bảo tính nhất quán của dữ liệu chia sẻ là thách thức lớn. Một tình huống thường gặp là sử dụng các primitive đồng bộ hóa như AutoResetEvent để điều phối luồng, nhưng vẫn gặp phải lỗi tranh chấp tài nguyên (race condition) do truy cập biến đếm không an toàn.

Xét kịch bản hai tác vụ cùng làm việc trên một mảng dữ liệu và một biến chỉ mục chung. Đoạn mã dưới đây mô phỏng việc một luồng tăng chỉ số và luồng kia ghi dữ liệu vào vị trí tương ứng:


var signalGate = new AutoResetEvent(false);
int counter = 0;
int[] buffer = new int[100000];

var workerA = Task.Run(() =>
{
    Console.WriteLine("Worker A started");
    while (counter < 99999)
    {
        counter++;
        signalGate.WaitOne();
    }
    Console.WriteLine("Worker A finished");
});

var workerB = Task.Run(() =>
{
    Console.WriteLine("Worker B started");
    while (counter < 99999)
    {
        buffer[counter] = counter;
        signalGate.Set();
    }
    Console.WriteLine("Worker B finished");
});

Task.WaitAll(workerA, workerB);

int errorCount = 0;
for (int k = 0; k < 99999; ++k)
{
    if (buffer[k] != k)
    {
        errorCount++;
    }
}

Console.WriteLine($"Tỷ lệ lỗi: {errorCount * 1.0 / 99999}");

Kết quả thực thi cho thấy tỷ lệ lỗi khác 0, điều này xảy ra do luồng workerB có thể truy cập biến counter trước khi luồng workerA hoàn tất việc đồng bộ. Tốc độ thực thi của các luồng không đồng đều dẫn đến việc đọc ghi chồng lấn.

Một phương án cải tiến thường được nghĩ đến là sử dụng hai sự kiện để tạo cơ chế bắt tay (handshake) chặt chẽ hơn:


var gate1 = new AutoResetEvent(true);
var gate2 = new AutoResetEvent(false);
int index = 0;
int[] storage = new int[100000];

var producer = Task.Run(() =>
{
    Console.WriteLine("Producer running");
    while (index < 99999)
    {
        gate1.WaitOne();
        index++;
        gate2.Set();
    }
    Console.WriteLine("Producer done");
});

var consumer = Task.Run(() =>
{
    Console.WriteLine("Consumer running");
    while (index < 99999)
    {
        gate2.WaitOne();
        storage[index] = index;
        gate1.Set();
    }
    Console.WriteLine("Consumer done");
});

Task.WaitAll(producer, consumer);

Mặc dù cách tiếp cận này sử dụng hai cờ hiệu, nhưng việc kiểm tra điều kiện vòng lặp while (index < limit) vẫn nằm ngoài vùng bảo vệ của sự kiện. Điều này dẫn đến khả năng một luồng thoát vòng lặp trong khi luồng kia vẫn đang xử lý, gây ra truy cập ngoài phạm vi hoặc bỏ sót dữ liệu.

Phiên bản tiếp thử nghiệm thay đổi thứ tự chờ và kích hoạt sự kiện, chỉ sử dụng một điểm đồng bộ duy nhất:


var syncPoint = new AutoResetEvent(false);
int position = 0;
int[] registry = new int[100000];

var taskIncrement = Task.Run(() =>
{
    while (position < 99999)
    {
        syncPoint.WaitOne();
        ++position;
    }
});

var taskWrite = Task.Run(() =>
{
    while (position < 99999)
    {
        registry[position] = position;
        syncPoint.Set();
    }
});

Task.WaitAll(taskIncrement, taskWrite);

int mismatch = 0;
for (int j = 0; j < 99999; ++j)
{
    if (registry[j] != j)
    {
        mismatch++;
        Console.WriteLine($"Lỗi tại index {j}: giá trị {registry[j]}");
    }
}

Vấn đề cốt lõi không nằm ở thứ tự gọi WaitOne hay Set, mà nằm ở việc biến position được đọc để kiểm tra điều kiện vòng lặp mà không có cơ chế khóa (lock) hoặc từ khóa volatile đảm bảo tính cập nhật tức thời giữa các luồng. Ngay cả việc thay đổi toán tử tăng giá trị từ ++i sang i++ cũng không giải quyết được vấn đề vì cả hai thao tác này đều không nguyên tử (non-atomic) trong môi trường đa luồng:


var lockObj = new AutoResetEvent(false);
int[] map = new int[100000];
int cursor = 0;

var updater = Task.Run(() =>
{
    while (cursor < 99999)
    {
        lockObj.WaitOne();
        cursor++; 
    }
});

var writer = Task.Run(() =>
{
    while (cursor < 99999)
    {
        map[cursor] = cursor;
        lockObj.Set();
    }
});

Task.WaitAll(updater, writer);

Thẻ: csharp multithreading autoresetevent race-condition Concurrency

Đăng vào ngày 6 tháng 7 lúc 08:59