Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 45.228 phút đọc

Race condition và mutex

Sau bài này bạn sẽ làm được

  • Tái hiện race condition và giải thích nguyên nhân
  • Sửa bằng mutex và giữ vùng tới hạn ngắn
  • Dùng công cụ phát hiện đua dữ liệu
  • Đo được kết quả sai khác nhau mỗi lần chạy

dem++ nhìn như một thao tác, nhưng máy làm ba bước. Khi hai luồng xen kẽ giữa ba bước đó, một lần tăng bị mất. Đây là race condition, và nó là lỗi khó tái hiện nhất trong lập trình đa luồng.

#Race condition

race.c, bốn luồng cùng tăng một biến
#include <pthread.h>
#include <stdio.h>

#define N   4
#define LAN 1000000

static long dem = 0;

static void *tang(void *a) {
    (void)a;
    for (int i = 0; i < LAN; ++i) dem++;     /* KHONG nguyen tu */
    return NULL;
}

int main(void) {
    pthread_t t[N];
    for (int i = 0; i < N; ++i) pthread_create(&t[i], NULL, tang, NULL);
    for (int i = 0; i < N; ++i) pthread_join(t[i], NULL);

    printf("mong doi %d, thuc te %ld\n", N * LAN, dem);
    return 0;
}
terminal
gcc -O0 -pthread race.c -o race.exe
# Chay 3 lan: ket qua SAI va KHAC NHAU moi lan
for r in 1 2 3; do ./race.exe; done
mong doi 4000000, thuc te 1116038
mong doi 4000000, thuc te 1211321
mong doi 4000000, thuc te 1138675

#Vì sao xảy ra

Race condition
Lỗi khi kết quả phụ thuộc vào thứ tự tương đối mà các luồng chạy. Với dem++, hai luồng đọc cùng một giá trị, mỗi luồng cộng một, rồi cùng ghi lại, làm mất một lần tăng.
/* dem++ gom BA buoc may:
     1. load  dem tu bo nho vao thanh ghi
     2. add   1 vao thanh ghi
     3. store thanh ghi tro lai dem

   Hai luong co the xen ke giua ba buoc: */

  Luong A: load dem (=5)
  Luong B: load dem (=5)         <- ca hai doc CUNG gia tri 5
  Luong A: add 1 -> 6, store 6
  Luong B: add 1 -> 6, store 6   <- MAT mot lan tang: le ra 7

/* Hai lan tang chi lam dem tang mot. Voi hang trieu lan tren
   bon luong, hang trieu lan tang bi mat, nen ket qua thap hon
   nhieu so voi mong doi.

   Van de: giua buoc load va store cua mot luong, luong khac chen
   vao. "Vung toi han" (dem++) can duoc bao ve de mot luc chi mot
   luong o trong do. Do la viec cua mutex. */

#Mutex

Mutex
Mutual exclusion, khoá loại trừ lẫn nhau. Một luồng lock mutex trước khi vào vùng tới hạn và unlock khi ra. Trong lúc đó, luồng khác gọi lock phải chờ. Nhờ vậy chỉ một luồng ở trong vùng tới hạn tại một thời điểm.
mtx.c, sửa bằng mutex
#include <pthread.h>
#include <stdio.h>

#define N   4
#define LAN 1000000

static long dem = 0;
static pthread_mutex_t khoa = PTHREAD_MUTEX_INITIALIZER;

static void *tang(void *a) {
    (void)a;
    for (int i = 0; i < LAN; ++i) {
        pthread_mutex_lock(&khoa);
        dem++;                              /* VUNG TOI HAN */
        pthread_mutex_unlock(&khoa);
    }
    return NULL;
}

int main(void) {
    pthread_t t[N];
    for (int i = 0; i < N; ++i) pthread_create(&t[i], NULL, tang, NULL);
    for (int i = 0; i < N; ++i) pthread_join(t[i], NULL);
    printf("mong doi %d, thuc te %ld\n", N * LAN, dem);
    return 0;
}
terminal
gcc -O2 -pthread mtx.c -o mtx.exe && ./mtx.exe
mong doi 4000000, thuc te 4000000
API mutex
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;   /* khoi tao tinh */
/* hoac dong: */
pthread_mutex_init(&m, NULL);

pthread_mutex_lock(&m);        /* chan cho toi khi lay duoc khoa */
pthread_mutex_trylock(&m);     /* KHONG chan: tra EBUSY neu dang khoa */
pthread_mutex_unlock(&m);      /* mo khoa */
pthread_mutex_destroy(&m);     /* huy khi khong dung nua (chi voi init dong) */

#Vùng tới hạn ngắn

Quên unlock trên nhánh lỗi
pthread_mutex_lock(&m); if (co_loi) return -1; /* QUEN unlock: khoa bi giu MAI -> moi luong khac cho vo han (deadlock) */ dem++; pthread_mutex_unlock(&m);
Unlock ở mọi đường ra
pthread_mutex_lock(&m); int ma = 0; if (co_loi) { ma = -1; goto mo_khoa; } dem++; mo_khoa: pthread_mutex_unlock(&m); /* mo khoa o MOI duong ra */ return ma;

#Phát hiện bằng công cụ

Tự làm thử

  1. Chạy race.c ở -O0 mười lần và ghi lại kết quả khác nhau.
  2. Dịch cùng chương trình ở -O2 và quan sát lỗi có thể ẩn đi.
  3. Sửa bằng mutex và xác nhận kết quả luôn đúng.
  4. Đo tốc độ bản mutex so với bản không đồng bộ (sai) và giải thích chi phí.
  5. Trên Linux, chạy -fsanitize=thread và đọc báo cáo chỉ đúng dòng đua.
  6. Cố ý bỏ unlock trên một nhánh và quan sát chương trình treo.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • dem++ là ba bước máy; hai luồng xen kẽ làm mất lần tăng, và kết quả khác nhau mỗi lần chạy.
  • Ở -O2 lỗi có thể ẩn; chạy đúng một lần không chứng minh không có race.
  • Mutex bảo vệ vùng tới hạn: lock vào, unlock ra, chỉ một luồng ở trong tại một thời điểm.
  • Giữ vùng tới hạn ngắn nhất, chỉ bảo vệ dữ liệu chia sẻ, và unlock ở mọi đường ra.
  • ThreadSanitizer là cách duy nhất khẳng định có race; mọi biến chia sẻ có ghi đều cần đồng bộ.