Giao diện
⚠️ 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ĩa | Ví dụ |
|---|---|---|
| Data Race | ≥ 2 threads truy cập cùng memory, ít nhất 1 write, không có synchronization | counter++ ở trên |
| Race Condition | Kết quả phụ thuộc vào timing/thứ tự thực thi | Check-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
- ThreadSanitizer (TSan) — Compile với
-fsanitize=thread
bash
g++ -fsanitize=thread -g -o program program.cpp
./programOutput 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- Helgrind (Valgrind)
bash
valgrind --tool=helgrind ./programCác giải pháp (Preview)
| Giải pháp | Mô tả | When to use |
|---|---|---|
| Mutex | Khóa exclusive access | General purpose |
| Atomic | Lock-free operations | Simple counters, flags |
| Immutability | Không thay đổi data | Functional style |
| Thread-local | Mỗi thread có copy riêng | Caching, buffers |
| Message Passing | Không share state | Actor 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.