上篇把互斥锁和死锁聊完了。今天聊多线程协作的核心机制——条件变量。如果说mutex解决的是不要同时访问同一个数据的问题那条件变量解决的就是一个线程要等另一个线程做完某件事的问题。在机器人系统里这种等待的场景到处都是。传感器线程采集数据处理线程等数据到了再处理。控制线程算出指令执行线程等指令到了再发送。这就是经典的生产者-消费者模型。条件变量的基本用法先说一个反面例子。假设处理线程需要等传感器数据到了才能工作有人可能会这么写// 反面教材忙等浪费CPU void processThread() { while (running_) { if (hasNewData_) { processData(latestData_); hasNewData_ false; } // 没数据就空转CPU占用100% } }这叫忙等busy waitingCPU一直在空转检查什么都没干但吃满了核心。正确做法是用条件变量让线程在没有数据的时候睡着mutex mtx; condition_variable cv; queueSensorData dataQueue; // 生产者传感器线程 void sensorThread() { while (running_) { auto data readSensor(); { lock_guardmutex lock(mtx); dataQueue.push(data); } cv.notify_one(); // 通知等待的消费者 this_thread::sleep_for(10ms); } } // 消费者处理线程 void processThread() { while (running_) { SensorData data; { unique_lockmutex lock(mtx); cv.wait(lock, [] { return !dataQueue.empty() || !running_; }); if (!running_) break; data dataQueue.front(); dataQueue.pop(); } processData(data); } }条件变量的wait方法做了三件事释放锁、让线程进入等待状态、被唤醒后重新获取锁。第三个参数lambda表达式是谓词——只有当谓词为true时wait才会返回。这是为了防止虚假唤醒spurious wakeup操作系统有时候会无缘无故唤醒等待的线程有了谓词检查即使被虚假唤醒也会继续等。条件变量的两个通知方法notify_one()唤醒一个等待的线程。如果有多个线程在等具体唤醒哪个由操作系统决定。notify_all()唤醒所有等待的线程。所有被唤醒的线程会竞争锁一个一个来处理。什么时候用哪个如果只有一个消费者notify_one就够了。如果有多个消费者比如多个处理线程共享一个任务队列用notify_one让每次只有一个线程被唤醒去处理任务。如果你想让所有线程都收到通知比如系统要关闭了用notify_all。在机器人开发中的实际应用传感器数据缓冲是一个典型的生产者-消费者场景。激光雷达10Hz产生数据SLAM算法5Hz消费数据。频率不匹配需要一个缓冲区class SensorBuffer { mutex mtx_; condition_variable cv_; queueLidarScan buffer_; size_t maxSize_; public: SensorBuffer(size_t maxSz) : maxSize_(maxSz) {} void push(const LidarScan scan) { unique_lockmutex lock(mtx_); if (buffer_.size() maxSize_) { buffer_.pop(); // 满了就丢最旧的 } buffer_.push(scan); cv_.notify_one(); } LidarScan pop() { unique_lockmutex lock(mtx_); cv_.wait(lock, [this] { return !buffer_.empty(); }); auto scan buffer_.front(); buffer_.pop(); return scan; } // 带超时的版本 bool pop(LidarScan out, chrono::milliseconds timeout) { unique_lockmutex lock(mtx_); if (cv_.wait_for(lock, timeout, [this] { return !buffer_.empty(); })) { out buffer_.front(); buffer_.pop(); return true; } return false; // 超时了 } };wait_for是条件变量的超时版本——等一段时间如果条件满足了就返回true否则返回false。在实时性要求高的机器人系统里不能无限等下去超时机制很重要。多模块启动同步机器人系统启动的时候各个模块的初始化有依赖关系。比如导航模块必须等SLAM模块初始化完成才能开始工作。条件变量可以实现这种等待某个条件就绪的同步class SystemSync { mutex mtx_; condition_variable cv_; bool slamReady_ false; public: void onSlamReady() { lock_guardmutex lock(mtx_); slamReady_ true; cv_.notify_all(); // 通知所有等待的模块 } void waitForSlam() { unique_lockmutex lock(mtx_); cv_.wait(lock, [this] { return slamReady_; }); } };导航线程调waitForSlam()等待SLAM就绪SLAM初始化完成后调onSlamReady()通知所有等待者。比用sleep轮询优雅多了性能也更好很多推荐大家在实际项目中使用。面试中的关键考点条件变量为什么需要配合mutex使用因为wait操作本身必须是原子的——释放锁和进入等待状态必须是一个不可分割的操作。如果不配合mutex可能在检查完条件之后、进入等待之前生产者已经发了通知消费者就错过了。虚假唤醒是什么操作系统可能在没有notify的情况下唤醒等待的线程。这是POSIX线程标准的实现细节没法完全避免。所以wait必须带谓词检查被唤醒之后再确认条件是否真的满足。notify_one和notify_all的区别一个唤醒一个等待线程一个唤醒所有。选择取决于你的具体场景——如果是任务分发用notify_one如果是广播事件用notify_all。条件变量的调试经验条件变量相关的bug特别难排查因为它们往往是偶发的、不容易复现的。最常见的两个问题一是忘记用while循环或者谓词检查被虚假唤醒后数据还没准备好就往下走了二是notify的时候没持有锁导致通知丢失。遇到这类bug先在wait前后加日志打印队列状态和线程ID用thread_local标记每个线程的操作记录大部分问题都能快速定位到根因。GDB里可以用thread apply all bt查看所有线程的调用栈看哪个线程卡在wait上、哪个线程持有锁。给正在准备面试的你条件变量是多线程协作的基础设施和核心机制。面试里经常被要求手写一个完整的生产者-消费者模型——一个队列、一个mutex、一个条件变量、一个生产者线程、一个消费者线程。能写出来能讲清楚wait的谓词检查和虚假唤醒的原理就足够了。条件变量解决了线程间等待和通知的问题但每次操作都要加锁解锁。如果临界区很小比如只是一个计数器锁的开销就太大了。这时候就需要无锁编程了——下篇聊原子操作与内存序。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第55篇 多线程之互斥锁与死锁——一个真实死锁bug的排查过程 下一篇预告第57篇 多线程之原子操作与内存序——高性能无锁编程入门有任何问题欢迎评论区留言我会尽量回复。