Skip to content

🔒 Synchronization — mutex, lock_guard, unique_lock

Mutex (Mutual Exclusion) đảm bảo chỉ 1 thread có thể truy cập critical section tại một thời điểm.

std::mutex Basics

Cú pháp cơ bản

cpp
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // Mutex object
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();    // 🔒 Khóa — chờ nếu thread khác đang hold
        counter++;     // Critical section — an toàn
        mtx.unlock();  // 🔓 Mở khóa
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // ✅ 200000
    return 0;
}

Mutex Mental Model

┌─────────────────────────────────────────────────────────────────┐
│                    MUTEX VISUALIZATION                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Thread 1                    Thread 2                          │
│   ─────────                   ─────────                         │
│                                                                 │
│   mtx.lock();                                                   │
│   ┌──────────────┐                                              │
│   │   counter++  │            mtx.lock(); // ⏳ BLOCKED!        │
│   │  (trong)     │                        // Đợi Thread 1       │
│   └──────────────┘                                              │
│   mtx.unlock();                                                 │
│                               // Được chạy tiếp                  │
│                               ┌──────────────┐                  │
│                               │   counter++  │                  │
│                               └──────────────┘                  │
│                               mtx.unlock();                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Vấn đề với manual lock/unlock

Exception Safety

cpp
void dangerous() {
    mtx.lock();
    
    doSomething();  // ⚠️ Nếu throw exception...
    
    mtx.unlock();   // ... dòng này không được gọi!
                    // → DEADLOCK! Mutex bị locked mãi mãi
}

Quên unlock

cpp
void alsoDANGEROUS() {
    mtx.lock();
    
    if (someCondition) {
        return;     // ⚠️ Quên unlock!
    }
    
    mtx.unlock();
}

⚠️ NEVER USE lock()/unlock() DIRECTLY

Luôn dùng RAII wrappers như lock_guard hoặc unique_lock!


std::lock_guard — RAII Wrapper

Cú pháp

cpp
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        // ✅ RAII: Tự động lock khi tạo, unlock khi ra scope
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }  // 🔓 Tự động unlock ở đây, dù có exception hay không!
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // ✅ 200000
    return 0;
}

C++17: Class Template Argument Deduction (CTAD)

cpp
// C++11/14
std::lock_guard<std::mutex> lock(mtx);

// C++17: Compiler tự deduce template argument
std::lock_guard lock(mtx);

lock_guard Internals

cpp
// Simplified implementation
template<typename Mutex>
class lock_guard {
    Mutex& mutex_;
    
public:
    explicit lock_guard(Mutex& m) : mutex_(m) {
        mutex_.lock();  // Lock trong constructor
    }
    
    ~lock_guard() {
        mutex_.unlock();  // Unlock trong destructor
    }
    
    // Không cho copy
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
};

std::unique_lock — Flexible Locking

unique_lock giống lock_guard nhưng linh hoạt hơn:

Features

Featurelock_guardunique_lock
RAII
Manual unlock
Deferred locking
Try locking
Movable
Condition var

Basic Usage

cpp
#include <mutex>
#include <iostream>

std::mutex mtx;

void demo() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // Có thể unlock sớm
    lock.unlock();
    
    // ... làm việc không cần lock ...
    
    // Lock lại
    lock.lock();
    
    // ... làm việc cần lock ...
    
}  // Tự động unlock nếu đang locked

Deferred Locking

cpp
void deferredDemo() {
    // ❗ std::defer_lock: KHÔNG lock ngay
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    // ... chuẩn bị gì đó ...
    
    lock.lock();  // Lock sau
    
    // ... critical section ...
}

Try Locking

cpp
void tryLockDemo() {
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    
    if (lock.owns_lock()) {
        std::cout << "Got the lock!\n";
        // ... do work ...
    } else {
        std::cout << "Lock is busy, doing something else...\n";
    }
}

Timed Locking

cpp
#include <mutex>
#include <chrono>

std::timed_mutex tmtx;  // Phải dùng timed_mutex

void timedLockDemo() {
    std::unique_lock<std::timed_mutex> lock(tmtx, std::defer_lock);
    
    // Chờ tối đa 100ms
    if (lock.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "Got lock within timeout\n";
    } else {
        std::cout << "Timeout!\n";
    }
}

std::scoped_lock (C++17) — Multiple Mutexes

Vấn đề: Lock nhiều mutex

cpp
std::mutex mtx1, mtx2;

void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::lock_guard<std::mutex> lock2(mtx2);  // ⚠️ Deadlock risk!
    // ...
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::lock_guard<std::mutex> lock1(mtx1);  // ⚠️ Ngược thứ tự!
    // ...
}

Giải pháp C++17: scoped_lock

cpp
#include <mutex>

std::mutex mtx1, mtx2;

void safeMultipleLock() {
    // ✅ Lock cả 2 mutex atomically, không deadlock
    std::scoped_lock lock(mtx1, mtx2);
    
    // ... safe to access resources protected by both mutexes ...
}

Pre-C++17: std::lock()

cpp
void safeMultipleLockOld() {
    // std::lock() locks multiple mutexes without deadlock
    std::lock(mtx1, mtx2);
    
    // adopt_lock: Mutex đã locked, chỉ cần quản lý unlock
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    // ... critical section ...
}

Practical Example: Thread-safe Counter Class

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

class ThreadSafeCounter {
    mutable std::mutex mutex_;  // mutable để dùng trong const methods
    int value_ = 0;
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++value_;
    }
    
    void decrement() {
        std::lock_guard<std::mutex> lock(mutex_);
        --value_;
    }
    
    int get() const {
        std::lock_guard<std::mutex> lock(mutex_);
        return value_;
    }
};

int main() {
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;
    
    // 10 threads, mỗi thread increment 10000 lần
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 10000; ++j) {
                counter.increment();
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final count: " << counter.get() << std::endl;
    // Output: 100000 (always correct!)
    
    return 0;
}

std::recursive_mutex

Khi cùng một thread cần lock nhiều lần:

cpp
#include <mutex>
#include <iostream>

std::recursive_mutex rmtx;

void recursiveFunction(int depth) {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    
    std::cout << "Depth: " << depth << std::endl;
    
    if (depth > 0) {
        recursiveFunction(depth - 1);  // ✅ OK với recursive_mutex
    }
}

int main() {
    recursiveFunction(3);
    return 0;
}

⚠️ Khi nào dùng recursive_mutex?

  • Thường không khuyến khích — code complexity
  • Dùng khi refactor legacy code
  • Dùng trong recursive algorithms

std::shared_mutex (C++17) — Read-Write Lock

Cho phép multiple readers OR single writer:

cpp
#include <shared_mutex>
#include <mutex>
#include <iostream>

class ThreadSafeCache {
    mutable std::shared_mutex mutex_;
    std::unordered_map<int, std::string> cache_;
    
public:
    // Multiple readers allowed
    std::string read(int key) const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        auto it = cache_.find(key);
        return it != cache_.end() ? it->second : "";
    }
    
    // Exclusive write
    void write(int key, const std::string& value) {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        cache_[key] = value;
    }
};
┌─────────────────────────────────────────────────────────────────┐
│                   SHARED MUTEX BEHAVIOR                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  shared_lock (read):     ✅ ✅ ✅  Multiple readers OK           │
│                                                                 │
│  unique_lock (write):    ✅ ❌ ❌  Only 1 writer, blocks all     │
│                                                                 │
│  Read + Write:           ❌        Writers wait for readers,    │
│                                    readers wait for writers     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Best Practices

1. Minimize Lock Duration

cpp
// ❌ BAD: Lock quá lâu
void bad() {
    std::lock_guard<std::mutex> lock(mtx);
    prepareData();      // Không cần lock
    processData();      // Cần lock
    logResults();       // Không cần lock
}

// ✅ GOOD: Lock ngắn nhất có thể
void good() {
    prepareData();
    
    {
        std::lock_guard<std::mutex> lock(mtx);
        processData();
    }  // Unlock ngay
    
    logResults();
}

2. Avoid Calling Unknown Code While Holding Lock

cpp
// ❌ DANGEROUS
void risky() {
    std::lock_guard<std::mutex> lock(mtx);
    callback();  // callback có thể cũng lock mtx → DEADLOCK!
}

// ✅ SAFE
void safe() {
    std::vector<Something> localCopy;
    {
        std::lock_guard<std::mutex> lock(mtx);
        localCopy = sharedData;
    }
    
    for (auto& item : localCopy) {
        callback(item);  // Safe, không hold lock
    }
}

3. Use Appropriate Lock Type

SituationLock Type
Simple scope-basedlock_guard
Need unlock/relockunique_lock
Multiple mutexesscoped_lock (C++17)
Read-heavyshared_lock (C++17)
Condition variablesunique_lock

📚 Tổng kết

ClassWhen to Use
std::mutexBasic mutual exclusion
std::lock_guardSimple RAII locking
std::unique_lockFlexible (deferred, manual, timed)
std::scoped_lockMultiple mutexes (C++17)
std::shared_mutexRead-write locks (C++17)
std::recursive_mutexSame thread locks multiple times

➡️ Tiếp theo

Bạn đã biết cách dùng mutex để tránh race conditions. Nhưng có một cái bẫy nguy hiểm hơn đang chờ...

Deadlocks → — Bài toán các Triết gia ăn tối.

🧠 Quiz

Câu 1: std::lock_guardstd::unique_lock khác nhau chủ yếu ở điểm nào?

  • [ ] A) lock_guard nhanh hơn unique_lock đáng kể
  • [x] B) unique_lock có thể unlock/lock lại thủ công, lock_guard thì không
  • [ ] C) lock_guard hỗ trợ nhiều mutex, unique_lock chỉ một
  • [ ] D) Không có sự khác biệt, chỉ là syntax khác nhau

💡 Giải thích: std::lock_guard là RAII wrapper đơn giản — lock khi tạo, unlock khi hủy, không thể thay đổi. std::unique_lock linh hoạt hơn: hỗ trợ deferred locking, try_lock, timed lock, và có thể unlock/lock lại. Dùng lock_guard khi chỉ cần simple scoped locking, unique_lock khi cần flexibility.

Câu 2: std::scoped_lock giải quyết vấn đề gì so với nhiều lock_guard?

  • [ ] A) Tốc độ lock nhanh hơn
  • [x] B) Tránh deadlock bằng cách lock nhiều mutex cùng lúc theo thứ tự nhất quán
  • [ ] C) Sử dụng ít bộ nhớ hơn
  • [ ] D) Hỗ trợ recursive locking

💡 Giải thích: Khi cần lock nhiều mutex, việc lock theo thứ tự khác nhau ở các thread gây deadlock. std::scoped_lock (C++17) lock tất cả mutex cùng lúc bằng thuật toán tránh deadlock, tương tự std::lock() nhưng kết hợp RAII. Đây là best practice khi cần lock 2+ mutex.

Câu 3: Race condition xảy ra khi nào?

  • [ ] A) Khi hai thread cùng đọc một biến shared
  • [x] B) Khi ít nhất một thread ghi vào shared data mà không có synchronization
  • [ ] C) Khi dùng quá nhiều mutex trong chương trình
  • [ ] D) Khi thread bị block quá lâu bởi mutex

💡 Giải thích: Race condition xảy ra khi hai hoặc nhiều threads truy cập cùng shared data đồng thời, và ít nhất một thread thực hiện ghi (write), mà không có cơ chế đồng bộ hóa. Nhiều threads cùng đọc (read-only) thì hoàn toàn an toàn. Đây là lý do std::shared_mutex cho phép nhiều readers nhưng chỉ một writer.