文章目录彻底搞懂 C 锁、条件变量和生产者-消费者模式一、锁mutex到底是什么二、两种锁的区别三、条件变量condition_variable不用条件变量——轮询浪费 CPU用条件变量——数据没到就睡到了自动醒wait 到底做了什么notify_one 和 notify_all为什么 wait 需要一个条件 lambda四、生产者-消费者模式最简单版本为什么两个消费者交替取数据关键细节五、我项目里的 BlockingQueue六、常见面试问题速答Q: lock_guard 和 unique_lock 的区别Q: 条件变量为什么必须配合 while 或 lambdaQ: 为什么 notify 放在 unlock 后面Q: 为什么 push 加锁了pop 的 wait 还能解锁七、我记住的关键规则彻底搞懂 C 锁、条件变量和生产者-消费者模式一、锁mutex到底是什么锁不是代码层面的东西是约定。你和室友住一间房门上挂一把锁——谁进去谁锁门出来再开。另一个人想进去门口等着。#includemutexstd::mutex mtx;// 一把锁void写日志(conststd::stringmsg){mtx.lock();// 锁门// 往文件里写日志这个操作不能同时被两个线程做mtx.unlock();// 开门}如果两个线程同时往同一个文件写日志内容会交叠错乱。加锁后同一时刻只有一个线程在写。但手动lock()/unlock()容易出事——写日志写到一半抛异常了unlock()没执行锁永远开不了所有人卡死。所以 C 提供了管家void写日志(conststd::stringmsg){std::lock_guardstd::mutexlock(mtx);// 构造 立刻加锁// 写日志...}// lock 析构 自动解锁就算中间抛异常也一定解锁lock_guard 构造加锁离开花括号自动解锁。永远用它不要手动调 lock/unlock。二、两种锁的区别lock_guardunique_lock加锁时机构造时自动构造时自动解锁时机离开{}自动离开{}自动也可以手动unlock()能临时解锁再重新加锁吗❌✅能配合条件变量吗❌✅用在哪简单临界区条件变量 / 需要灵活控制的场景// lock_guard省心绝大多数场景用它{std::lock_guardstd::mutexlock(mtx);// 操作共享数据}// unique_lock灵活条件变量必须用它{std::unique_lockstd::mutexlock(mtx);// 操作共享数据lock.unlock();// 提前解锁lock_guard 做不到// 可以做其他不涉及共享数据的操作lock.lock();// 重新加锁}三、条件变量condition_variable锁解决了同时只能一个人访问的问题但解决不了数据还没到先睡会儿的问题。不用条件变量——轮询浪费 CPUwhile(queue_.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(10));// 每隔 10ms 检查一次CPU 空转反应慢}autotaskqueue_.front();queue_.pop();用条件变量——数据没到就睡到了自动醒std::unique_lockstd::mutexlock(mtx);cv.wait(lock,[]{return!queue_.empty();});// 空了就睡0% CPUautotaskqueue_.front();queue_.pop();wait 到底做了什么cv.wait(lock,[]{return!queue_.empty();});① 检查条件 !queue_.empty() → ② true → 直接返回什么都不发生 ③ false → 解锁 lock → 线程挂起睡觉0% CPU ④ 另一个线程调 cv.notify_one() 叫醒我 ⑤ 重新拿锁 → 再检查条件 → true 就取数据关键睡觉前解锁醒了重新拿锁。如果睡觉时还抱着锁不放别人怎么往队列里放东西这就是死锁。notify_one 和 notify_allcv.notify_one();// 叫醒一个正在 wait 的线程cv.notify_all();// 叫醒所有正在 wait 的线程为什么 wait 需要一个条件 lambda因为虚假唤醒——操作系统偶尔会莫名其妙叫醒线程极少见但标准允许。还有一个原因是你通知了一个线程但这期间另一个线程可能先醒、先把数据取走了。所以醒过来第一件事必须是检查条件不满足条件继续睡。四、生产者-消费者模式这是多线程编程里最常见的模式。一个或多个线程生产数据放到队列里一个或多个线程从队列里取数据处理。最简单版本#includecondition_variable#includeiostream#includemutex#includequeue#includethread#includechronostd::mutex mtx;std::condition_variable cv;std::queueintq;boolrunningtrue;voidconsumer(intid){while(running||!q.empty()){std::unique_lockstd::mutexlock(mtx);// 队列空且还在生产 → 睡觉等cv.wait(lock,[]{return!q.empty()||!running;});// runningfalse 且队列空 → 退出if(!runningq.empty())break;intvalq.front();q.pop();lock.unlock();// 数据到手就解锁不拖std::cout消费者id 取走: valstd::endl;std::this_thread::sleep_for(std::chrono::milliseconds(200));}}voidproducer(){for(inti1;i10;i){{std::lock_guardstd::mutexlock(mtx);q.push(i);std::cout生产者 放入: istd::endl;}// 离开 {} 自动解锁cv.notify_one();// 叫醒一个消费者std::this_thread::sleep_for(std::chrono::milliseconds(100));}runningfalse;cv.notify_all();// 叫醒所有人下班了别睡了}intmain(){std::threadc1(consumer,1);std::threadc2(consumer,2);std::threadp(producer);c1.join();// 等线程结束c2.join();p.join();std::cout全部结束std::endl;return0;}运行结果大概是生产者 放入: 1 消费者 1 取走: 1 生产者 放入: 2 消费者 2 取走: 2 生产者 放入: 3 消费者 1 取走: 3 ... 全部结束为什么两个消费者交替取数据因为sleep_for(200ms)让取完数据的消费者暂时不继续抢锁另一个消费者有时间拿到锁。去掉 sleep 就是纯抢锁谁抢到谁拿就不一定交替了。关键细节{std::lock_guardstd::mutexlock(mtx);q.push(i);}// ← 先解锁cv.notify_one();// ← 再通知不要抱着锁通知为什么解锁后才通知如果先通知再解锁——被叫醒的线程立刻尝试拿锁但锁还没解又得等。多此一举。解锁后通知人家醒了直接能拿到锁。五、我项目里的 BlockingQueue项目里把这些东西封装成了一个类templatetypenameTclassBlockingQueue{public:voidpush(T item){{std::lock_guardstd::mutexlock(mutex_);queue_.push(std::move(item));}cv_.notify_one();// 解锁后通知}Tpop(){std::unique_lockstd::mutexlock(mutex_);// 用 unique_lockcv_.wait(lock,[]{return!queue_.empty();});// 空了就睡T itemstd::move(queue_.front());queue_.pop();returnitem;}private:std::mutex mutex_;std::condition_variable cv_;std::queueTqueue_;};用法简单// 生产者odud 的 IPC 接收线程tx_queue_.push(OduSerialTask{...});// 消费者odud 的串口发送线程OduSerialTask tasktx_queue_.pop();// 空了自动睡不占 CPU六、常见面试问题速答Q: lock_guard 和 unique_lock 的区别A: lock_guard 只有构造加锁 析构解锁不能手动控制。unique_lock 可以临时 unlock 再 lock条件变量的 wait 必须用 unique_lock。大部分场景用 lock_guard 就够了。Q: 条件变量为什么必须配合 while 或 lambdaA: 因为虚假唤醒。线程可能被操作系统莫名其妙叫醒也可能被 notify 但数据被另一个线程抢了。醒过来必须重新检查条件不满足继续睡。Q: 为什么 notify 放在 unlock 后面A: 先 unlock 再 notify——被叫醒的线程直接能拿到锁不用再等。反过来先 notify 再 unlock被叫醒的线程发现锁还没解又要等浪费一次上下文切换。Q: 为什么 push 加锁了pop 的 wait 还能解锁A: wait 内部做的事是临时解锁 → 挂起 → 被叫醒 → 重新加锁。push 在 wait 解锁期间可以拿到锁、放数据。这正是条件变量的核心机制。七、我记住的关键规则任何共享数据被多线程读写 → 加锁。用lock_guard别手写 lock/unlock。加锁后不做耗时操作。拿数据就解锁别抱着锁做 IO。生产者-消费者 队列 锁 条件变量。生产者 push notify消费者 pop wait。wait 必须传条件判断。要么 lambda要么 while 套 wait。防止虚假唤醒。先解锁再 notify。不抱着锁通知别人。
彻底搞懂 C++ 锁、条件变量和生产者-消费者模式
发布时间:2026/7/9 3:19:58
文章目录彻底搞懂 C 锁、条件变量和生产者-消费者模式一、锁mutex到底是什么二、两种锁的区别三、条件变量condition_variable不用条件变量——轮询浪费 CPU用条件变量——数据没到就睡到了自动醒wait 到底做了什么notify_one 和 notify_all为什么 wait 需要一个条件 lambda四、生产者-消费者模式最简单版本为什么两个消费者交替取数据关键细节五、我项目里的 BlockingQueue六、常见面试问题速答Q: lock_guard 和 unique_lock 的区别Q: 条件变量为什么必须配合 while 或 lambdaQ: 为什么 notify 放在 unlock 后面Q: 为什么 push 加锁了pop 的 wait 还能解锁七、我记住的关键规则彻底搞懂 C 锁、条件变量和生产者-消费者模式一、锁mutex到底是什么锁不是代码层面的东西是约定。你和室友住一间房门上挂一把锁——谁进去谁锁门出来再开。另一个人想进去门口等着。#includemutexstd::mutex mtx;// 一把锁void写日志(conststd::stringmsg){mtx.lock();// 锁门// 往文件里写日志这个操作不能同时被两个线程做mtx.unlock();// 开门}如果两个线程同时往同一个文件写日志内容会交叠错乱。加锁后同一时刻只有一个线程在写。但手动lock()/unlock()容易出事——写日志写到一半抛异常了unlock()没执行锁永远开不了所有人卡死。所以 C 提供了管家void写日志(conststd::stringmsg){std::lock_guardstd::mutexlock(mtx);// 构造 立刻加锁// 写日志...}// lock 析构 自动解锁就算中间抛异常也一定解锁lock_guard 构造加锁离开花括号自动解锁。永远用它不要手动调 lock/unlock。二、两种锁的区别lock_guardunique_lock加锁时机构造时自动构造时自动解锁时机离开{}自动离开{}自动也可以手动unlock()能临时解锁再重新加锁吗❌✅能配合条件变量吗❌✅用在哪简单临界区条件变量 / 需要灵活控制的场景// lock_guard省心绝大多数场景用它{std::lock_guardstd::mutexlock(mtx);// 操作共享数据}// unique_lock灵活条件变量必须用它{std::unique_lockstd::mutexlock(mtx);// 操作共享数据lock.unlock();// 提前解锁lock_guard 做不到// 可以做其他不涉及共享数据的操作lock.lock();// 重新加锁}三、条件变量condition_variable锁解决了同时只能一个人访问的问题但解决不了数据还没到先睡会儿的问题。不用条件变量——轮询浪费 CPUwhile(queue_.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(10));// 每隔 10ms 检查一次CPU 空转反应慢}autotaskqueue_.front();queue_.pop();用条件变量——数据没到就睡到了自动醒std::unique_lockstd::mutexlock(mtx);cv.wait(lock,[]{return!queue_.empty();});// 空了就睡0% CPUautotaskqueue_.front();queue_.pop();wait 到底做了什么cv.wait(lock,[]{return!queue_.empty();});① 检查条件 !queue_.empty() → ② true → 直接返回什么都不发生 ③ false → 解锁 lock → 线程挂起睡觉0% CPU ④ 另一个线程调 cv.notify_one() 叫醒我 ⑤ 重新拿锁 → 再检查条件 → true 就取数据关键睡觉前解锁醒了重新拿锁。如果睡觉时还抱着锁不放别人怎么往队列里放东西这就是死锁。notify_one 和 notify_allcv.notify_one();// 叫醒一个正在 wait 的线程cv.notify_all();// 叫醒所有正在 wait 的线程为什么 wait 需要一个条件 lambda因为虚假唤醒——操作系统偶尔会莫名其妙叫醒线程极少见但标准允许。还有一个原因是你通知了一个线程但这期间另一个线程可能先醒、先把数据取走了。所以醒过来第一件事必须是检查条件不满足条件继续睡。四、生产者-消费者模式这是多线程编程里最常见的模式。一个或多个线程生产数据放到队列里一个或多个线程从队列里取数据处理。最简单版本#includecondition_variable#includeiostream#includemutex#includequeue#includethread#includechronostd::mutex mtx;std::condition_variable cv;std::queueintq;boolrunningtrue;voidconsumer(intid){while(running||!q.empty()){std::unique_lockstd::mutexlock(mtx);// 队列空且还在生产 → 睡觉等cv.wait(lock,[]{return!q.empty()||!running;});// runningfalse 且队列空 → 退出if(!runningq.empty())break;intvalq.front();q.pop();lock.unlock();// 数据到手就解锁不拖std::cout消费者id 取走: valstd::endl;std::this_thread::sleep_for(std::chrono::milliseconds(200));}}voidproducer(){for(inti1;i10;i){{std::lock_guardstd::mutexlock(mtx);q.push(i);std::cout生产者 放入: istd::endl;}// 离开 {} 自动解锁cv.notify_one();// 叫醒一个消费者std::this_thread::sleep_for(std::chrono::milliseconds(100));}runningfalse;cv.notify_all();// 叫醒所有人下班了别睡了}intmain(){std::threadc1(consumer,1);std::threadc2(consumer,2);std::threadp(producer);c1.join();// 等线程结束c2.join();p.join();std::cout全部结束std::endl;return0;}运行结果大概是生产者 放入: 1 消费者 1 取走: 1 生产者 放入: 2 消费者 2 取走: 2 生产者 放入: 3 消费者 1 取走: 3 ... 全部结束为什么两个消费者交替取数据因为sleep_for(200ms)让取完数据的消费者暂时不继续抢锁另一个消费者有时间拿到锁。去掉 sleep 就是纯抢锁谁抢到谁拿就不一定交替了。关键细节{std::lock_guardstd::mutexlock(mtx);q.push(i);}// ← 先解锁cv.notify_one();// ← 再通知不要抱着锁通知为什么解锁后才通知如果先通知再解锁——被叫醒的线程立刻尝试拿锁但锁还没解又得等。多此一举。解锁后通知人家醒了直接能拿到锁。五、我项目里的 BlockingQueue项目里把这些东西封装成了一个类templatetypenameTclassBlockingQueue{public:voidpush(T item){{std::lock_guardstd::mutexlock(mutex_);queue_.push(std::move(item));}cv_.notify_one();// 解锁后通知}Tpop(){std::unique_lockstd::mutexlock(mutex_);// 用 unique_lockcv_.wait(lock,[]{return!queue_.empty();});// 空了就睡T itemstd::move(queue_.front());queue_.pop();returnitem;}private:std::mutex mutex_;std::condition_variable cv_;std::queueTqueue_;};用法简单// 生产者odud 的 IPC 接收线程tx_queue_.push(OduSerialTask{...});// 消费者odud 的串口发送线程OduSerialTask tasktx_queue_.pop();// 空了自动睡不占 CPU六、常见面试问题速答Q: lock_guard 和 unique_lock 的区别A: lock_guard 只有构造加锁 析构解锁不能手动控制。unique_lock 可以临时 unlock 再 lock条件变量的 wait 必须用 unique_lock。大部分场景用 lock_guard 就够了。Q: 条件变量为什么必须配合 while 或 lambdaA: 因为虚假唤醒。线程可能被操作系统莫名其妙叫醒也可能被 notify 但数据被另一个线程抢了。醒过来必须重新检查条件不满足继续睡。Q: 为什么 notify 放在 unlock 后面A: 先 unlock 再 notify——被叫醒的线程直接能拿到锁不用再等。反过来先 notify 再 unlock被叫醒的线程发现锁还没解又要等浪费一次上下文切换。Q: 为什么 push 加锁了pop 的 wait 还能解锁A: wait 内部做的事是临时解锁 → 挂起 → 被叫醒 → 重新加锁。push 在 wait 解锁期间可以拿到锁、放数据。这正是条件变量的核心机制。七、我记住的关键规则任何共享数据被多线程读写 → 加锁。用lock_guard别手写 lock/unlock。加锁后不做耗时操作。拿数据就解锁别抱着锁做 IO。生产者-消费者 队列 锁 条件变量。生产者 push notify消费者 pop wait。wait 必须传条件判断。要么 lambda要么 while 套 wait。防止虚假唤醒。先解锁再 notify。不抱着锁通知别人。