1. 核心概念:为什么需要锁?

在多线程程序中,当多个线程需要访问共享数据(Shared Data) 时,如果至少有一个线程要修改(写)这个数据,就会发生数据竞争(Data Race)。数据竞争会导致未定义行为(程序崩溃、计算结果错误、诡异的问题等)。

锁(Lock) 是一种同步原语(Synchronization Primitive),用于强制保证在任意时刻,只有一个线程可以执行被锁保护的代码段(称为临界区,Critical Section),从而避免数据竞争。

核心思想:将可能导致数据竞争的“读-写”或“写-写”操作变为原子操作。


2. C++标准库中的锁家族

C++标准库在 <mutex> 头文件中提供了多种互斥量和管理类。下图清晰地展示了它们之间的关系:

在这里插入图片描述

2.1 互斥量类型 (Mutex Types)

这是真正的资源,负责最基础的互斥功能。

类型说明使用场景
std::mutex最基本的互斥量。不可递归上锁(同一线程重复加锁会导致死锁)。最通用的场景,需要独占访问共享资源时。
std::recursive_mutex递归互斥量。允许同一线程多次加锁,解锁次数需与加锁次数相同。需要从递归函数中访问共享资源,或者调用可能(再次)加锁的成员函数。性能稍差于std::mutex,应优先使用std::mutex
std::timed_mutex带超时功能的互斥量。除了lock(),还提供了try_lock_for()try_lock_until()需要尝试获取锁,如果获取不到不想无限期等待,而是做别的事情时。
std::recursive_timed_mutex带超时功能的递归互斥量。以上两者的结合。同时需要递归和超时功能的复杂场景。
std::shared_mutex (C++17)共享互斥量。实现了读写锁(Reader-Writer Lock) 模型。读多写少的场景。允许多个读者同时读,但写者必须独占访问。

2.2 锁管理类 (Lock Management)

这是RAII(Resource Acquisition Is Initialization) 理念的体现,用于管理互斥量的锁定和解锁。这是你应该优先使用的方式,它能保证在任何情况下(包括异常发生),锁都能被正确释放,避免死锁。

类型说明使用场景
std::lock_guard (C++11)作用域锁。构造时加锁,析构时解锁。非常轻量,效率高。最简单的临界区。不需要额外操作(如手动解锁、条件变量)时,首选
std::unique_lock (C++11)功能更丰富的锁管理。比lock_guard功能多,但稍重一点。支持: 1. 延迟加锁 2. 手动解锁 3. 条件变量(必须配合unique_lock) 4. 所有权转移需要更灵活控制锁的场景,例如: 1. 与std::condition_variable配合使用。 2. 不需要立刻锁定的情况。 3. 需要提前解锁以减小锁的粒度。
std::shared_lock (C++14)共享锁管理。用于std::shared_mutex的“读锁”(共享锁)。配合std::shared_mutex,在只读临界区使用。
std::scoped_lock (C++17)多互斥量作用域锁。增强版的lock_guard,可以同时锁住多个互斥量而不死锁。需要同时获取多个锁的场景。它使用死锁避免算法,比按顺序调用lock()更安全。

3. 详细用法与代码示例

示例 1: 最基本的 std::mutexstd::lock_guard

这是最常用、最简单的组合。

cpp

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

std::mutex g_mutex;
int shared_data = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        // 进入作用域时构造lock_guard,自动加锁
        std::lock_guard<std::mutex> lock(g_mutex);
        // 从这里开始是临界区
        ++shared_data;
        // lock_guard析构,自动解锁
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Final value: " << shared_data << std::endl; // 总是 200000
    return 0;
}

示例 2: 灵活的 std::unique_lock 和条件变量

cpp

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    // 等待条件满足。wait()会临时解锁,避免生产者无法获取锁
    cv.wait(lock, []{ return data_ready; });
    std::cout << "Consumed!" << std::endl;
}

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true;
    } // lock_guard在这里析构解锁
    cv.notify_one(); // 通知消费者
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    
    t1.join();
    t2.join();
    return 0;
}

示例 3: 读写锁 std::shared_mutex (C++17)

cpp

#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex rw_mutex;
int resource = 0;

void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        // 使用 shared_lock 获取读锁(共享锁)
        std::shared_lock<std::shared_mutex> lock(rw_mutex);
        // 多个读者可以同时进入这个临界区
        std::cout << "Reader " << id << ": sees " << resource << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer(int id) {
    for (int i = 0; i < 5; ++i) {
        // 使用 unique_lock 或 lock_guard 获取写锁(独占锁)
        std::unique_lock<std::shared_mutex> lock(rw_mutex);
        // 一次只有一个写者可以进入这个临界区
        ++resource;
        std::cout << "Writer " << id << ": wrote " << resource << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::thread readers[3];
    std::thread writers[2];

    for (int i = 0; i < 2; ++i) writers[i] = std::thread(writer, i);
    for (int i = 0; i < 3; ++i) readers[i] = std::thread(reader, i);

    for (auto& w : writers) w.join();
    for (auto& r : readers) r.join();

    return 0;
}

示例 4: 同时锁多个互斥量 std::scoped_lock (C++17)

避免多个锁因顺序问题导致的死锁。

cpp

#include <mutex>

std::mutex mutex1, mutex2;
int a, b; // 需要同时保护的两个数据

void safe_swap(int& x, int& y) {
    // 使用scoped_lock一次性锁住两个互斥量,内部使用死锁避免算法
    std::scoped_lock lock(mutex1, mutex2);
    // 等效于老式的:
    // std::lock(mutex1, mutex2); // 同时锁住,避免死锁
    // std::lock_guard<std::mutex> lk1(mutex1, std::adopt_lock);
    // std::lock_guard<std::mutex> lk2(mutex2, std::adopt_lock);
    std::swap(a, b);
}

void unsafe_swap(int& x, int& y) {
    // 危险!如果另一个线程以相反顺序锁,会导致死锁!
    std::lock_guard<std::mutex> lk1(mutex1);
    std::lock_guard<std::mutex> lk2(mutex2);
    std::swap(a, b);
}

4. 选择锁的决策流程

面对一个需要同步的场景,你可以遵循以下决策树来选择最合适的工具:

在这里插入图片描述


5. 重要注意事项和最佳实践

  1. 避免死锁(Deadlock)
    • 保证互斥量的上锁顺序一致。(std::scoped_lock 可以帮你解决这个问题)
    • 尽量不要在持有锁的情况下调用未知的外部函数(它可能也会锁,导致顺序混乱)。
    • 如果需要获取多个锁,使用std::lockstd::scoped_lock一次性锁住。
  2. 缩小临界区:锁的粒度要细。只锁住真正共享的数据和操作,锁住后尽快释放,不要在做I/O操作、耗时计算时还持有锁。
  3. 优先使用RAII管理类:总是优先使用std::lock_guard, std::unique_lock等,而不是手动调用lock()unlock()
  4. 不要返回共享数据的指针或引用:锁保护的是数据,而不是指针。如果你将受保护数据的引用/指针返回给外部,锁就形同虚设了。
  5. 性能考量:加锁和解锁是有开销的。对于极简单的操作(如++counter),可以考虑使用std::atomic无锁编程,性能更高。

6. 练习题

练习 1: std::mutexstd::lock_guard

问题:创建一个程序,启动3个线程。每个线程都需要向标准输出(std::cout)顺序地打印10条消息(格式:[Thread ID: <id>] Message: <i>)。如果不加同步,这些消息会交错在一起。请使用 std::mutexstd::lock_guard 来同步输出,确保每个线程的消息都是连续且不被打断的。

目标:掌握最基本的互斥量和使用RAII管理锁。


练习 2: std::recursive_mutex

问题:实现一个简单的线程安全的计数器类 RecursiveCounter。它有一个 int 成员 count_ 和两个方法:

  1. void increment(): 将计数器加1。
  2. void increment_twice(): 调用两次 increment() 方法。
    main 函数中,创建多个线程来调用 increment_twice。如果使用 std::mutexincrement_twice 中的第二次 increment() 调用会导致死锁。请使用 std::recursive_mutex 来避免这个问题。

目标:理解递归互斥量的应用场景——可重入代码。


练习 3: std::timed_mutex

问题:模拟一个需要获取控制权的任务。一个线程(“Controller”)持有 std::timed_mutex 并模拟长时间工作(例如睡眠5秒)。另一个线程(“Client”)尝试使用 try_lock_for() 获取锁,等待最多1秒。如果1秒内获取成功,打印“Successfully took control”;否则,打印“Failed to take control, doing something else…”,然后执行其他任务。

目标:掌握带超时功能的互斥量,用于避免长时间阻塞。


练习 4: std::shared_mutex (读写锁)

问题:模拟一个配置缓存。创建一个 ConfigCache 类,内部用一个 std::map<std::string, int> 存储配置项。该类应提供三个线程安全的方法:

  1. int get(const std::string& key): 读取配置(允许多个读者同时读)。
  2. void set(const std::string& key, int value): 设置/更新配置(写者必须独占)。
  3. void print_all(): 打印所有配置(读取操作)。
    创建多个读者线程和一个写者线程来演示读写锁的行为。读者线程应频繁调用 getprint_all,写者线程间歇性地调用 set

目标:理解读写锁的优势(读多写少场景下的性能提升)。


练习 5: std::unique_lockstd::condition_variable

问题:实现一个简单的生产者-消费者模型。创建一个固定大小的队列(例如,大小为5)。

  • 生产者线程:生产数据(例如,递增的数字),如果队列已满,则等待。
  • 消费者线程:消费数据,如果队列为空,则等待。
    使用 std::mutexstd::unique_lockstd::condition_variable 来同步生产者和消费者。std::unique_lock 的灵活性(如手动解锁)对于条件变量是必需的。

目标:掌握 unique_lock 与条件变量配合实现复杂的线程间同步。


练习 6: std::scoped_lock

问题:模拟一个需要同时更新两个独立账户的银行转账操作。有两个 Account 对象,各有一个 std::mutex 和一个 balance 成员。编写一个线程安全的 transfer(Account& from, Account& to, int amount) 函数。为了正确转账,必须同时锁住两个账户的互斥量,否则可能发生死锁(例如,线程A锁了账户1准备锁账户2,同时线程B锁了账户2准备锁账户1)。请使用 std::scoped_lock 来安全地同时获取两个锁。

目标:掌握如何安全地同时获取多个互斥量,避免死锁。


7. 解决方案源码

解决方案 1: std::mutexstd::lock_guard

cpp

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

std::mutex g_print_mutex;

void print_messages(int thread_id) {
    for (int i = 0; i < 10; ++i) {
        std::lock_guard<std::mutex> lock(g_print_mutex); // 进入临界区加锁
        std::cout << "[Thread ID: " << thread_id << "] Message: " << i << std::endl;
    } // 退出作用域,lock_guard析构,自动解锁
}

int main() {
    std::thread t1(print_messages, 1);
    std::thread t2(print_messages, 2);
    std::thread t3(print_messages, 3);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

解决方案 2: std::recursive_mutex

cpp

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

class RecursiveCounter {
private:
    int count_ = 0;
    std::recursive_mutex mutex_;

public:
    void increment() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        ++count_;
    }

    void increment_twice() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        // 因为 mutex_ 是递归的,所以即使在已经上锁的情况下再次调用 increment() 也没问题。
        increment(); // 调用一次
        increment(); // 再调用一次
    }

    int get_count() const {
        // 注意:为了线程安全地读取,这里也需要加锁。
        // 但 const 函数不能修改成员,所以需要使用 mutable 声明 mutex_
        // 本例为简化,直接返回,实际生产环境需注意。
        return count_;
    }
};

int main() {
    RecursiveCounter counter;
    std::thread t1([&counter]() { counter.increment_twice(); });
    std::thread t2([&counter]() { counter.increment_twice(); });

    t1.join();
    t2.join();

    std::cout << "Final count: " << counter.get_count() << std::endl; // 应该是 4
    return 0;
}
// 注意:需要在类定义中将 mutable std::recursive_mutex mutex_; 才能让 get_count  const 函数加锁

解决方案 3: std::timed_mutex

cpp

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex g_timed_mutex;

void controller_work() {
    std::cout << "Controller started, locking the mutex for 5s..." << std::endl;
    std::lock_guard<std::timed_mutex> lock(g_timed_mutex);
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "Controller finished, releasing the mutex." << std::endl;
}

void client_work() {
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 确保Controller先拿到锁

    std::cout << "Client trying to get the lock for 1s..." << std::endl;
    if (g_timed_mutex.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "Successfully took control!" << std::endl;
        g_timed_mutex.unlock();
    } else {
        std::cout << "Failed to take control, doing something else..." << std::endl;
    }
}

int main() {
    std::thread controller(controller_work);
    std::thread client(client_work);

    controller.join();
    client.join();

    return 0;
}

解决方案 4: std::shared_mutex (读写锁)

cpp

#include <iostream>
#include <thread>
#include <shared_mutex>
#include <map>
#include <chrono>

class ConfigCache {
private:
    std::map<std::string, int> data_;
    mutable std::shared_mutex mutex_; // mutable 允许 const 成员函数上读锁

public:
    int get(const std::string& key) const {
        std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁(共享)
        auto it = data_.find(key);
        if (it != data_.end()) {
            // 模拟读取耗时
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            return it->second;
        }
        return -1; // Not found
    }

    void set(const std::string& key, int value) {
        std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁(独占)
        // 模拟写入耗时
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        data_[key] = value;
        std::cout << "Set " << key << " = " << value << std::endl;
    }

    void print_all() const {
        std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁(共享)
        std::cout << "ConfigCache contents: ";
        for (const auto& pair : data_) {
            std::cout << pair.first << ":" << pair.second << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    ConfigCache cache;

    // 写者线程
    std::thread writer([&cache]() {
        for (int i = 0; i < 3; ++i) {
            cache.set("key" + std::to_string(i), i * 100);
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
        }
    });

    // 读者线程
    std::thread reader1([&cache]() {
        for (int i = 0; i < 10; ++i) {
            int val = cache.get("key1");
            std::cout << "Reader1 got key1: " << val << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    });

    std::thread reader2([&cache]() {
        for (int i = 0; i < 5; ++i) {
            cache.print_all();
            std::this_thread::sleep_for(std::chrono::milliseconds(250));
        }
    });

    writer.join();
    reader1.join();
    reader2.join();

    return 0;
}

解决方案 5: std::unique_lockstd::condition_variable

cpp

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>

const int MAX_QUEUE_SIZE = 5;

std::queue<int> g_queue;
std::mutex g_queue_mutex;
std::condition_variable g_cv_producer; // 生产者条件变量:队列不满
std::condition_variable g_cv_consumer; // 消费者条件变量:队列不空

void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(g_queue_mutex);

        // 如果队列满,等待(释放锁,被通知后重新获取锁)
        g_cv_producer.wait(lock, [] { return g_queue.size() < MAX_QUEUE_SIZE; });

        // 生产数据
        int data = id * 100 + i;
        g_queue.push(data);
        std::cout << "Produced: " << data << " (Queue size: " << g_queue.size() << ")" << std::endl;

        lock.unlock(); // 提前解锁,减小临界区
        g_cv_consumer.notify_one(); // 通知一个消费者

        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void consumer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(g_queue_mutex);

        // 如果队列空,等待
        g_cv_consumer.wait(lock, [] { return !g_queue.empty(); });

        // 消费数据
        int data = g_queue.front();
        g_queue.pop();
        std::cout << "Consumer " << id << " consumed: " << data << std::endl;

        lock.unlock();
        g_cv_producer.notify_one(); // 通知一个生产者

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::thread p1(producer, 1);
    std::thread c1(consumer, 1);
    std::thread c2(consumer, 2);

    p1.join();
    c1.join();
    c2.join();

    return 0;
}

解决方案 6: std::scoped_lock

cpp

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

struct Account {
    std::mutex mutex;
    int balance;
    Account(int initial_balance) : balance(initial_balance) {}
};

void transfer(Account& from, Account& to, int amount) {
    if (&from == &to) return; // 自我转账检查

    // 关键:使用 std::scoped_lock 一次性锁住两个互斥量
    // 它内部使用死锁避免算法,无论顺序如何都不会死锁
    std::scoped_lock lock(from.mutex, to.mutex);

    // 检查发送方余额是否足够
    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance += amount;
        std::cout << "Transferred " << amount << " from Account" << &from << " to Account" << &to << std::endl;
    } else {
        std::cout << "Insufficient funds in Account" << &from << " for transfer of " << amount << std::endl;
    }
} // scoped_lock 在这里析构,自动释放两个锁

int main() {
    Account acc1(1000);
    Account acc2(500);

    std::cout << "Initial balances: " << std::endl;
    std::cout << "Account1: " << acc1.balance << std::endl;
    std::cout << "Account2: " << acc2.balance << std::endl;

    // 启动多个线程进行转账,可能涉及不同的账户顺序
    std::thread t1([&]() { transfer(acc1, acc2, 200); }); // A -> B
    std::thread t2([&]() { transfer(acc2, acc1, 50); });  // B -> A

    t1.join();
    t2.join();

    std::cout << "Final balances: " << std::endl;
    std::cout << "Account1: " << acc1.balance << std::endl;
    std::cout << "Account2: " << acc2.balance << std::endl;

    return 0;
}

更多推荐