C++11 同步机制:互斥锁和条件变量

C++11 同步机制:互斥锁和条件变量

编码文章call10242025-07-01 14:37:164A+A-

前段时间,我研究了 ROS2(Jazzy)机器人开发系统,并将官网中比较重要的教程和概念,按照自己的学习顺序翻译成了中文,进行了整理和记录。到目前为止,已经整理了20多篇文章。如果你想回顾之前的内容,可以查阅主页中 ROS2(Jazzy)相关文章。

在研究 ROS2 的过程中,我发现它使用了不少 C++11 的新特性。这让我意识到,深入掌握这些特性对于深入理解 ROS2 的实现原理和优化代码非常重要。

因此,我开启了 C++11 系列。目前已经完成了以下几篇:

  1. C++11 ROS2性能狂飙:C++11移动语义‘偷梁换柱’实战
  2. C++11 Lambda 表达式 以及 std::function和std::bind
  3. C++11 智能指针:unique_ptr、shared_ptr和weak_ptr
  4. C++11 的线程管理(std::thread)
  5. 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_guardstd::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();
    }
};

注意事项与最佳实践

  1. 条件变量的使用要求
  • 必须使用 std::unique_locklock_guard无法满足解锁/重锁需求)
  • 通知时不要求持有锁(但通常建议持有)
  1. 注意通知丢失问题
  • 不要遗漏发出notify_one()notify_all()通知
  • wait()调用前发起通知会导致通知丢失
  • 解决方案:始终在修改条件后立即通知
  1. 性能优化
  • 优先使用notify_one()可以减少上下文切换
  • 仅在需要唤醒所有线程时用notify_all()
  1. 必要时使用超时等待
std::cv_status status = cv.wait_for(lock, 100ms, predicate);
if (status == std::cv_status::timeout) {
   // 超时处理
}

条件变量是C++并发编程的核心同步原语,正确使用时能高效解决复杂线程协调问题。结合RAII锁和谓词检查,可构建出健壮且高效的并发系统。


欢迎关注 【智践行】 一起学习机器人开发,发送【C++】获得学习资料。

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4