Skip to content

⚠️ Race Conditions — Khi Mọi Thứ Đổ Vỡ

Race Condition xảy ra khi kết quả phụ thuộc vào thứ tự thực thi không xác định của các threads.

Real-world Analogy: ATM Withdrawal

Tưởng tượng có 2 người cùng rút tiền từ một tài khoản (balance = 1000đ):

┌─────────────────────────────────────────────────────────────────┐
│                    ATM RACE CONDITION                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Account Balance: 1000đ                                         │
│                                                                 │
│  Person A                          Person B                     │
│  ─────────                         ─────────                    │
│  1. Read balance: 1000đ                                         │
│                                    2. Read balance: 1000đ       │
│  3. Withdraw 600đ                                               │
│     New balance = 400đ                                          │
│                                    4. Withdraw 600đ             │
│                                       New balance = 400đ        │
│  5. Write 400đ to account                                       │
│                                    6. Write 400đ to account     │
│                                                                 │
│  ❌ Final balance: 400đ (should be: 1000 - 600 - 600 = -200đ)   │
│  ❌ Ngân hàng mất 600đ!                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Đây chính xác là điều xảy ra trong multithreaded programs!


Demo: The Broken Counter

💥 Code có Race Condition

cpp
#include <iostream>
#include <thread>

// ⚠️ SHARED MUTABLE STATE — nguyên nhân của mọi rắc rối
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // ⚠️ DATA RACE!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Expected: 200000\n";
    std::cout << "Actual:   " << counter << std::endl;
    
    return 0;
}

Kết quả thực tế

bash
# Chạy nhiều lần, kết quả KHÁC nhau mỗi lần!
$ ./broken_counter
Expected: 200000
Actual:   143287

$ ./broken_counter
Expected: 200000
Actual:   156421

$ ./broken_counter
Expected: 200000
Actual:   189012

⚠️ UNDEFINED BEHAVIOR

Đây là Undefined Behavior theo C++ standard. Chương trình có thể crash, produce wrong results, hoặc "work" by accident.


Tại sao xảy ra?

counter++ không phải atomic!

cpp
counter++;  // Trông đơn giản, nhưng thực tế là 3 bước:
┌─────────────────────────────────────────────────────────────────┐
│                 counter++ = 3 OPERATIONS                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. READ:   Đọc giá trị counter từ memory vào register          │
│  2. MODIFY: Tăng giá trị trong register lên 1                   │
│  3. WRITE:  Ghi giá trị mới từ register về memory               │
│                                                                 │
│  Assembly (x86):                                                │
│  ────────────────                                               │
│  mov eax, [counter]    ; READ                                   │
│  add eax, 1            ; MODIFY                                 │
│  mov [counter], eax    ; WRITE                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Interleaved Execution Timeline

┌─────────────────────────────────────────────────────────────────┐
│         INTERLEAVED EXECUTION (counter starts at 5)             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Time    Thread 1              Thread 2              Memory     │
│  ─────   ─────────             ─────────             ────────   │
│  t1      READ counter=5                              counter=5  │
│  t2                            READ counter=5        counter=5  │
│  t3      MODIFY reg=6                                counter=5  │
│  t4                            MODIFY reg=6          counter=5  │
│  t5      WRITE 6                                     counter=6  │
│  t6                            WRITE 6               counter=6  │
│                                                                 │
│  ❌ Kết quả: counter = 6 (đáng lẽ phải là 7!)                   │
│  ❌ Một increment bị "mất"                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Data Race vs Race Condition

Khái niệmĐịnh nghĩaVí dụ
Data Race≥ 2 threads truy cập cùng memory, ít nhất 1 write, không có synchronizationcounter++ ở trên
Race ConditionKết quả phụ thuộc vào timing/thứ tự thực thiCheck-then-act pattern

Check-then-act Race Condition

cpp
#include <fstream>
#include <thread>
#include <filesystem>

void writeToFile(const std::string& filename) {
    // ⚠️ RACE CONDITION: Check và Act không atomic
    if (!std::filesystem::exists(filename)) {  // CHECK
        // Thread khác có thể tạo file ở đây!
        std::ofstream file(filename);          // ACT
        file << "Data from thread " 
             << std::this_thread::get_id() << std::endl;
    }
}

int main() {
    std::thread t1(writeToFile, "data.txt");
    std::thread t2(writeToFile, "data.txt");
    t1.join();
    t2.join();
    return 0;
}

Thêm ví dụ: Linked List Corruption

cpp
#include <iostream>
#include <thread>

struct Node {
    int data;
    Node* next;
};

Node* head = nullptr;

void insertFront(int value) {
    Node* newNode = new Node{value, nullptr};
    
    // ⚠️ RACE CONDITION trong 2 bước này:
    newNode->next = head;  // 1. Đọc head
    head = newNode;        // 2. Ghi head
    
    // Nếu 2 threads chạy đồng thời, một node có thể bị "mất"!
}

int main() {
    std::thread t1([]() {
        for (int i = 0; i < 1000; ++i) insertFront(i);
    });
    
    std::thread t2([]() {
        for (int i = 1000; i < 2000; ++i) insertFront(i);
    });
    
    t1.join();
    t2.join();
    
    // Đếm nodes
    int count = 0;
    for (Node* curr = head; curr != nullptr; curr = curr->next) {
        count++;
    }
    
    std::cout << "Expected: 2000, Actual: " << count << std::endl;
    // Output: Expected: 2000, Actual: 1847 (hoặc số khác < 2000)
    
    return 0;
}

Memory Visibility Issues

Ngoài interleaving, còn có vấn đề memory visibility:

┌─────────────────────────────────────────────────────────────────┐
│                    CPU CACHE PROBLEM                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│        ┌─────────┐                    ┌─────────┐               │
│        │  CPU 0  │                    │  CPU 1  │               │
│        │ ┌─────┐ │                    │ ┌─────┐ │               │
│        │ │Cache│ │                    │ │Cache│ │               │
│        │ │x = 5│ │                    │ │x = 5│ │               │
│        │ └─────┘ │                    │ └─────┘ │               │
│        └────┬────┘                    └────┬────┘               │
│             │                              │                    │
│             └──────────────┬───────────────┘                    │
│                            │                                    │
│                     ┌──────┴──────┐                             │
│                     │    RAM      │                             │
│                     │   x = 5     │                             │
│                     └─────────────┘                             │
│                                                                 │
│  Thread 1 trên CPU 0:     Thread 2 trên CPU 1:                  │
│  x = 10; (cache only!)    if (x == 10) { ... }                  │
│                           // ❌ Có thể không thấy x = 10!       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

⚠️ CPU Cache

Mỗi CPU core có cache riêng. Thay đổi trong cache của CPU 0 không tự động visible cho CPU 1 nếu không có memory barriers.


Compiler Reordering

Compiler có thể reorder instructions để optimize:

cpp
// Code bạn viết:
int data = 0;
bool ready = false;

// Thread 1
void producer() {
    data = 42;      // (1)
    ready = true;   // (2)
}

// Thread 2
void consumer() {
    while (!ready);  // Chờ
    std::cout << data << std::endl;
}

// ⚠️ Compiler có thể reorder thành:
// ready = true;   // (2) moved up!
// data = 42;      // (1)

// Kết quả: consumer có thể thấy ready=true nhưng data vẫn = 0!

Detecting Race Conditions

Tools

  1. ThreadSanitizer (TSan) — Compile với -fsanitize=thread
bash
g++ -fsanitize=thread -g -o program program.cpp
./program

Output khi có data race:

WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x... by thread T2:
    #0 increment() race.cpp:8
  Previous write of size 4 at 0x... by thread T1:
    #0 increment() race.cpp:8
  1. Helgrind (Valgrind)
bash
valgrind --tool=helgrind ./program

Các giải pháp (Preview)

Giải phápMô tảWhen to use
MutexKhóa exclusive accessGeneral purpose
AtomicLock-free operationsSimple counters, flags
ImmutabilityKhông thay đổi dataFunctional style
Thread-localMỗi thread có copy riêngCaching, buffers
Message PassingKhông share stateActor model

📚 Tổng kết

┌─────────────────────────────────────────────────────────────────┐
│                    RACE CONDITION CHECKLIST                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ⚠️ Nguy hiểm khi:                                              │
│     □ Shared mutable state                                      │
│     □ ≥ 2 threads access                                        │
│     □ Ít nhất 1 thread writes                                   │
│     □ Không có synchronization                                  │
│                                                                 │
│  ✅ An toàn khi:                                                │
│     □ Chỉ read (immutable data)                                 │
│     □ Mỗi thread có data riêng (thread-local)                   │
│     □ Có proper synchronization (mutex, atomic)                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

⚠️ GOLDEN RULE

Shared mutable state + Multiple threads = 💥 Bugs

Luôn hỏi: "Có nhiều threads nào có thể truy cập biến này không?"


➡️ Tiếp theo

Giờ bạn đã hiểu vấn đề, hãy học cách fix bằng synchronization primitives!

Synchronization → — mutex, lock_guard, unique_lock.