Skip to main content

Command Palette

Search for a command to run...

Cách đồng bộ dữ liệu bằng Mutex trong đa luồng

Updated
4 min read

Mutex là gì ?

Mutex là chìa khóa để bảo vệ tài nguyên chung, tại một thời điểm chỉ cho phép một luồng truy cập. Nếu coi Tài nguyên chung là một cái phòng có một cửa thì Mutex chính là cái chì khóa phòng. Luồng nào lấy được chìa khóa và đi vào phòng thì các luồng còn lại phải chờ ở ngoài. Chỉ khi luồng đang giữ chìa khóa mở cửa đi ra thì luồng khác mới có thể vào.

Chúng ta cần phân biệt kernel-space mutex (trên Windows được goi bằng API CreateMutex) và user-space mutex (C++ với thư viện std::mutex, CRITICAL_SECTION của windows).

Kernel-space mutex là một Kernel object có phạm vi toàn hệ thống (system-wide) được dùng để đồng bộ các tiến trình trong hệ điều hành. Mỗi khi mutex này lock/unlock, CPU phải thực hiện mode switch để chuyển từ user-mode sang kernel-mode, do đó chi phí để tạo mutex ở mức kernel là lớn.

User-space mutex có cấu trúc và chi phí triển khai nhẹ hơn phù hợp để đồng bộ các luồng trong cùng một tiến trình. Khi gọi loại mutex này nếu như không xảy ra tranh chấp thì sẽ không cần gọi vào kernel mode. Do đó chi phí trung bình nhỏ hơn nhiều.

Ở phạm vi lập trình ứng dụng, đồng bộ các luồng của cùng một tiến trình ở bài viết này, chúng ta sẽ nói về user-space mutex.

Cùng xem xét một ví dụ sau viết bằng c++ trên windows, sử dụng win32 api để tạo thread.

#include <process.h>
#include <windows.h>
#include <iostream>

// Hàm xử lý của thread 1
unsigned __stdcall t1_worker(void* args) {
    while (true)
    {
        std::cout << "Thread " << "1 " << "is running ... \n";
        Sleep(1000);
    }
    return 0;
}

// Hàm xử lý của thread 2
unsigned __stdcall t2_worker(void* args) {
    while (true)
    {
        std::cout << "Thread " << "2 " << "is running ... \n";
        Sleep(1000);
    }
    return 0; 
}

int main() {
    // Khởi tạo thread 1
    uintptr_t t1_Handle = _beginthreadex(NULL, 0, t1_worker, NULL, 0, 0);

    // Khởi tạo thread 2
    uintptr_t t2_Handle = _beginthreadex(NULL, 0, t2_worker, NULL, 0, 0);

    // Chờ cho hai thread kết thúc
    // Thực tế 2 thread sẽ chạy mãi trong vòng lặp while(true)
    WaitForSingleObject((HANDLE)t1_Handle, INFINITE);
    WaitForSingleObject((HANDLE)t2_Handle, INFINITE);

    // Đóng các thread handler
    CloseHandle((HANDLE)t1_Handle);
    CloseHandle((HANDLE)t2_Handle);

    return 0;
}

Kết quả: ở lần chạy thứ 6 của vòng lặp while, khi thread 2 đang sử dụng đối tượng std::cout để ghi ra console chữ “Thread 2“ thì window schedule (bộ lập lịch) đã dừng thread hiện tại và chuyển CPU sang thread 1.

Std:cout là đối tượng tài nguyên dùng chung (shared resource), ở ví dụ trên đã có hiện tượng race condition xảy ra, hai thread tranh nhau sử dụng để in ra màn hình console.

Để không xảy ra vấn đề này chúng ta sử dụng Mutex để đảm bảo mỗi thread ghi chuỗi hoàn tất thì các thread khác mới được quyền sử dụng đối tượng std::cout.

Trong C++ sử dụng thư viện std::mutex để triển khai như sau.

#include <process.h>
#include <windows.h>
#include <iostream>
#include <mutex>

// Khai báo đối tượng mutex toàn cục
std::mutex g_Mutex;

// Hàm xử lý của thread 1
unsigned __stdcall t1_worker(void* args) {
    while (true)
    {
        {
            // Sử dụng lock_guard để tự động mở khóa
            std::lock_guard<std::mutex> lock(g_Mutex);
            std::cout << "Thread " << "1 " << "is running ... \n";
        }
        Sleep(1000);
    }
    return 0;
}

// Hàm xử lý của thread 2
unsigned __stdcall t2_worker(void* args) {
    while (true)
    {
        {
            // Sử dụng lock_guard để tự động mở khóa
            std::lock_guard<std::mutex> lock(g_Mutex);
            std::cout << "Thread " << "2 " << "is running ... \n";
        }
        Sleep(1000);
    }
    return 0;
}

int main() {
    // Khởi tạo thread 1
    uintptr_t t1_Handle = _beginthreadex(NULL, 0, t1_worker, NULL, 0, 0);
    if(t1_Handle == 0) {
        std::cout << "Lỗi khởi tạo thread 1\n";
        return 1;
    }

    // Khởi tạo thread 2
    uintptr_t t2_Handle = _beginthreadex(NULL, 0, t2_worker, NULL, 0, 0);
    if(t2_Handle == 0) {
        std::cout << "Lỗi khởi tạo thread 2\n";
        return 1;
    }

    // Chờ cho hai thread kết thúc
    // Thực tế 2 thread sẽ chạy mãi trong vòng lặp while(true)
    WaitForSingleObject((HANDLE)t1_Handle, INFINITE);
    WaitForSingleObject((HANDLE)t2_Handle, INFINITE);

    // Đóng các thread handler
    CloseHandle((HANDLE)t1_Handle);
    CloseHandle((HANDLE)t2_Handle);

    return 0;
}

Kết quả đã không xảy ra tình trạng các chữ bị in chồng lấn như trước.

Lưu ý:

Mặc dù Mutex giúp đảm bảo tính đúng đắn của dữ liệu hoặc tranh chấp về tài nguyên dùng chung như std::cout chúng ta vừa khảo sát, nhưng nếu tranh chấp xảy ra nhiều, việc thực hiện Lock/Unlock quá thường xuyên sẽ làm thread bị block và phát sinh Context Switch gây chi phí lớn. Do đó, cần xem xét kỹ tần suất sử dụng và giữ thời gian chiếm giữ khóa (Lock time) ngắn nhất có thể để tránh làm giảm hiệu năng tổng thể của ứng dụng đa luồng.

Ở bài tiếp theo, chúng ta sẽ tìm hiểu Semaphore và cách sử dụng semaphore để đồng bộ hóa trong lập trình đa luồng.

The End.

More from this blog

U40 Học Code - Lập trình & Hệ điều hành

11 posts