C++11 同步机制:互斥锁和条件变量
前段时间,我研究了 ROS2(Jazzy)机器人开发系统,并将官网中比较重要的教程和概念,按照自己的学习顺序翻译成了中文,进行了整理和记录。到目前为止,已经整理了20多篇文章。如果你想回顾之前的内容,可以查阅主页中 ROS2(Jazzy)相关文章。
在研究 ROS2 的过程中,我发现它使用了不少 C++11 的新特性。这让我意识到,深入掌握这些特性对于深入理解 ROS2 的实现原理和优化代码非常重要。
因此,我开启了 C++11 系列。目前已经完成了以下几篇:
- C++11 ROS2性能狂飙:C++11移动语义‘偷梁换柱’实战
- C++11 Lambda 表达式 以及 std::function和std::bind
- C++11 智能指针:unique_ptr、shared_ptr和weak_ptr
- C++11 的线程管理(std::thread)
- C++11 原子操作 (std::atomic)
本文是关于同步机制互斥锁和条件变量的解读:
C++11 是并发编程的重要里程碑,引入了全面的线程支持和同步原语。这些同步机制用于协调多个线程的执行顺序,防止并发访问共享资源时出现数据竞争(Data Race) 和未定义行为。
一、互斥锁(Mutex)
一)什么是互斥锁
互斥锁(Mutex)是 C++ 中最基础的线程同步机制,用于保护共享资源,防止多个线程同时访问导致的数据竞争。同一时刻只有一个线程持有锁,所以当某个线程获得锁时,其他线程尝试获取会被阻塞或返回错误。而同一线程重复加锁会导致死锁(递归锁除外)。互斥锁依赖操作系统的原子指令,通过系统调用实现线程阻塞/唤醒。
二)互斥锁类型
std::mutex: 标准互斥锁(非递归)
std::recursive_mutex: 可重入互斥锁(同一线程可多次加锁,即递归锁)
std::timed_mutex: 支持超时机制的互斥锁
std::recursive_timed_mutex: 可重入+超时的互斥锁
三)互斥锁使用方法
#include <mutex>
std::mutex mtx;
// 基础操作
mtx.lock(); // 阻塞直到获取锁
mtx.unlock(); // 释放锁
bool success = mtx.try_lock(); // 非阻塞尝试获取锁
// 超时操作(仅限 timed_mutex)
std::timed_mutex tmtx;
bool success = tmtx.try_lock_for(std::chrono::milliseconds(100)); // 尝试100ms
bool success = tmtx.try_lock_until(std::chrono::steady_clock::now() + 100ms);
四)推荐使用 RAII 包装器
通常来说应该避免手动直接调用 lock()/unlock(),而是使用 RAII(Resource Acquisition Is Initialization)包装器自动管理锁的生命周期,避免忘记解锁。
std::lock_guard 和 std::unique_lock 这两个 RAII 包装器是 C++ 中管理锁的核心工具,用于自动加锁和解锁,但在功能和灵活性上有显著差异。
1.std::lock_guard
std::lock_guard 包装器在构造时加锁,析构时解锁(不可手动控制)。它是最基础的 RAII 包装器,几乎零开销,但是不支持移动或复制语义。
适用于简单临界区保护,或者是函数作用域内的资源保护。同时它也适用于性能敏感代码和不需要额外控制锁的场景。
{
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁
// 临界区操作...
} // 作用域结束自动解锁
2.std::unique_lock
std::unique_lock 是多功能锁管理器,支持手动加锁、解锁和查询锁。可以与条件变量配合使用,更加灵活。
并且std::unique_lock还具有以下锁定策略:
std::defer_lock_t // 延迟加锁(构造时不加锁)
std::try_to_lock_t // 尝试加锁(非阻塞)
std::adopt_lock_t // 接管已持有的锁
使用举例:
void advanced_function() {
// 延迟加锁(构造时不锁定)
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// ...非临界区代码...
lock.lock(); // 手动加锁
// 临界区代码...
lock.unlock(); // 可提前解锁
// ...其他操作...
if (need_reenter) {
lock.lock(); // 重新加锁
// ...
}
} // 析构时检查并自动解锁
五)预防死锁
1. 同时锁定多个互斥锁
std::mutex mtx1, mtx2;
// 安全方式:使用 std::lock 同时锁定(避免死锁算法)
{
std::unique_lock lock1(mtx1, std::defer_lock);
std::unique_lock lock2(mtx2, std::defer_lock);
std::lock(lock1, lock2); // 原子化锁定多个互斥锁
// 临界区操作...
} // 自动解锁
2. 固定加锁顺序
// 所有线程必须按固定顺序加锁(如先 mtx1 后 mtx2)
mtx1.lock();
mtx2.lock();
// 操作...
mtx2.unlock();
mtx1.unlock();
六)递归锁使用场景
std::recursive_mutex rmtx;
void funcA() {
std::lock_guard<std::recursive_mutex> lock(rmtx);
funcB(); // 调用同样需要锁的函数
}
void funcB() {
std::lock_guard<std::recursive_mutex> lock(rmtx); // OK
// ...
}
注意:一般来说递归锁通常暗示设计问题,应该优先考虑重构代码
七)性能优化建议
尽量缩小临界区:
// 错误:锁范围过大
{
std::lock_guard lock(mtx);
data = fetch_from_network(); // 耗时操作
process(data);
}
// 正确:仅保护必要部分
auto temp = fetch_from_network(); // 无锁操作
{
std::lock_guard lock(mtx);
process(temp);
}
八)典型错误示例
// 错误1:抛异常前忘记解锁
mtx.lock();
throw std::runtime_error("oops"); // 导致死锁
mtx.unlock();
// 错误2:递归死锁(非递归锁)
std::mutex mtx;
mtx.lock();
mtx.lock(); // 阻塞等待自己释放 → 死锁
// 错误3:不同线程解锁
std::thread t1([&]{
mtx.lock();
// ...
}); // 线程结束未解锁
std::thread t2([&]{
mtx.unlock(); // 未定义行为!
});
二、条件变量(Condition Variable)
条件变量(std::condition_variable)是C++中用于线程间同步的高级机制,允许线程在特定条件成立前阻塞等待,并在条件满足时被其他线程唤醒。它是实现复杂同步模式(如生产者-消费者)的核心工具。
条件变量的协作机制:
- 条件变量本身不存储状态,仅提供线程等待/唤醒机制
- 必须与互斥锁(mutex) 和共享条件配合使用
条件变量的关键操作
1. 等待操作(Waiting)
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 等待线程
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 条件检查lambda(predicate)
执行流程详解
1) 初始状态
- 线程已通过 lock 获得互斥锁
- 共享状态 ready == false
- 线程执行到 wait() 调用
2)内部操作序列(原子操作)
3)编译器实际上将 cv.wait(lock, predicate) 转换为:
while (!predicate()) { // 循环检查条件
cv.wait(lock); // 释放锁并等待
}
2. 通知操作(Notifying)
// 通知线程
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 修改共享条件
}
cv.notify_one(); // 或 notify_all()
- notify_one():唤醒一个等待线程
- notify_all():唤醒所有等待线程
典型应用场景
1. 生产者-消费者模型
std::queue<int> buffer;
const int MAX_SIZE = 10;
void producer() {
for (int i = 0; ; ++i) {
std::unique_lock lock(mtx);
// 等待缓冲区非满
cv_producer.wait(lock, []{
return buffer.size() < MAX_SIZE;
});
buffer.push(i);
cv_consumer.notify_one(); // 通知消费者
}
}
void consumer() {
while (true) {
std::unique_lock lock(mtx);
// 等待缓冲区非空
cv_consumer.wait(lock, []{
return !buffer.empty();
});
int data = buffer.front();
buffer.pop();
cv_producer.notify_one(); // 通知生产者
process(data);
}
}
2. 线程池任务分发
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::condition_variable task_cv;
void worker_thread() {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(mtx);
task_cv.wait(lock, [this]{
return !tasks.empty() || stop;
});
if (stop) break;
task = std::move(tasks.front());
tasks.pop();
}
task(); // 执行任务
}
}
public:
void enqueue(std::function<void()> task) {
{
std::lock_guard lock(mtx);
tasks.push(std::move(task));
}
task_cv.notify_one();
}
};
注意事项与最佳实践
- 条件变量的使用要求:
- 必须使用 std::unique_lock(lock_guard无法满足解锁/重锁需求)
- 通知时不要求持有锁(但通常建议持有)
- 注意通知丢失问题:
- 不要遗漏发出notify_one()或notify_all()通知
- 在wait()调用前发起通知会导致通知丢失
- 解决方案:始终在修改条件后立即通知
- 性能优化:
- 优先使用notify_one()可以减少上下文切换
- 仅在需要唤醒所有线程时用notify_all()
- 必要时使用超时等待:
std::cv_status status = cv.wait_for(lock, 100ms, predicate);
if (status == std::cv_status::timeout) {
// 超时处理
}
条件变量是C++并发编程的核心同步原语,正确使用时能高效解决复杂线程协调问题。结合RAII锁和谓词检查,可构建出健壮且高效的并发系统。
欢迎关注 【智践行】 一起学习机器人开发,发送【C++】获得学习资料。