在并发编程中,多个线程共享资源时,互斥同步是避免数据竞争和保证数据一致性的关键手段。C++11引入了一系列强大的线程和同步库,包括互斥(mutex)和条件变量(condition_variable)
本篇博客将详细介绍 C++11 中的互斥同步机制,重点包括 std::mutex、std::lock_guard、std::unique_lock 以及条件变量的使用场景和实现原理。
互斥锁:std::mutex
什么是 std::mutex
std::mutex 是 C++11 提供的最基本的互斥锁,用于保护共享资源,确保同一时刻只有一个线程能够访问该资源。它提供了 lock() 和 unlock() 两个基本方法,分别用于加锁和解锁。
std::mutex 的使用
1 |
|
在这个例子中,两个线程 t1 和 t2 都在对 counter 进行自增操作。通过 std::mutex 进行加锁和解锁,确保了同一时刻只有一个线程能够访问和修改 counter,避免了数据竞争。
存在的问题
虽然 std::mutex 提供了基础的互斥功能,但手动调用 lock() 和 unlock() 容易出错。例如,如果程序在 lock() 后发生异常或者忘记调用 unlock(),就可能导致死锁或资源长时间被占用。因此,C++11 提供了 std::lock_guard 和 std::unique_lock 来简化锁的管理。
std::lock_guard:自动管理锁的RAII机制
什么是 std::lock_guard
std::lock_guard 是 C++11 提供的 RAII(资源获取即初始化)机制的封装类,用于自动管理 mutex 的生命周期。构造时加锁,析构时解锁,确保在函数作用域结束时自动释放锁,避免了手动管理锁的复杂性。
使用 std::lock_guard
1 | void increment() { |
在上面的代码中,当 guard 对象离开作用域时,mutex 会被自动解锁,即使发生异常也不会忘记解锁,从而大大提升了代码的健壮性。
std::unique_lock:更灵活的锁管理
什么是 std::unique_lock
std::unique_lock 是一个比 std::lock_guard 更加灵活的锁管理类。与 std::lock_guard 不同,std::unique_lock 支持:
- 延迟锁定:可以在对象创建时不锁定,而在需要时调用
lock()。 - 提前解锁:可以手动调用
unlock()解除锁定。 - 通过
try_lock()尝试加锁,而不是等待锁定。
std::unique_lock 的使用
1 | void increment() { |
std::unique_lock 提供了更灵活的加锁和解锁控制,非常适合需要手动解锁或多次锁定的场景。尽管灵活性增加,但一般情况下仍推荐使用简单的 std::lock_guard,除非确实需要这种额外的灵活性。
条件变量:std::condition_variable
什么是 std::condition_variable
std::condition_variable 是一个同步机制,用于让一个线程等待另一个线程满足某个条件。条件变量通常与 std::unique_lock 一起使用,因为它要求有灵活的锁定和解锁操作。
条件变量的使用
1 |
|
在这个例子中,worker 线程在等待条件变量 cv,直到 ready 变为 true。signal 线程在修改 ready 后,通过 notify_one() 唤醒等待的线程。
wait 的工作原理
wait() 方法会先解锁传入的互斥锁,然后进入阻塞状态。当条件满足时,它会重新加锁并继续执行。条件变量常用于生产者-消费者模型或多线程任务调度。
其他C++11同步机制
除了 std::mutex 及其相关工具,C++11 还提供了其他几种常用的同步机制:
std::recursive_mutex:允许同一线程多次加锁,而不会导致死锁。std::timed_mutex:支持超时的mutex,可以使用try_lock_for或try_lock_until进行定时加锁。std::shared_mutex:允许多个线程共享读锁定,适用于读多写少的场景。