拓冰建站拓冰建站
首页 / 资讯中心 / 正文

细讲C++ 【8】:多线程下的互斥锁与条件变量常见误区

一、基础理论知识点1.1 条件变量定义与互斥锁底层误区核心笔记1.1.1 条件变量基础概念条件变量是一种线程同步机制用于在线程间进行通知和等待操作。 它通常与互斥锁一起使用允许线程在某些条件不满足时等待在条件满足时被其他线程唤醒。 条件变量必须和互斥锁全局变量搭配使用。1.1.2 互斥锁四大核心认知互斥锁不会绑定、锁住任何资源仅充当令牌。多线程争抢同一把锁抢到的线程执行其余线程阻塞。互斥锁不存在系统强制资源保护线程安全依靠编码约定访问共享资源前必须抢占同一把锁。线程仅在获取锁的环节发生阻塞锁获取之前的代码能够正常运行。锁释放后所有阻塞线程公平竞争阻塞时长不影响抢锁优先级。1.1.3 三类线程阻塞/睡眠机制区分std::this_thread::sleep_for 睡眠 线程主动暂停指定时长主动让出CPU全程持有已获取的互斥锁不会释放无法被其他线程提前唤醒只能等待计时结束仅用作单纯延时禁止持有锁时调用会直接引发死锁。单纯互斥锁抢锁阻塞无条件变量 抢锁失败后线程排队阻塞不持有目标锁锁持有者释放锁后操作系统自动唤醒排队线程竞争无需手动发送通知。cv.wait 条件变量休眠阻塞 调用wait会自动释放传入的unique_lock线程进入条件变量休眠队列锁释放不会自动唤醒必须调用notify_one、notify_all才能唤醒无通知则线程永久阻塞。二、代码分拆单一知识点一级知识点二级标题代码块三级笔记2.1 unique_lock与lock_guard区分完整代码段#includeiostream using namespace std; #includemutex #includecondition_variable std::mutex mtx; std::condition_variable cv; int i 0; void WaitDemo() { std::unique_lockstd::mutex locker(mtx); while (i 0) { cv.wait(locker); } } int main() { std::thread t(WaitDemo); t.join(); return 0; }核心笔记unique_lock是C11智能锁管理类作用域结束自动释放锁省去手动解锁操作。 初始化默认直接上锁。 cv.wait仅支持unique_lock无法使用lock_guardwait执行过程中需要临时解锁、重新加锁lock_guard不支持动态解锁操作。2.2 cv.wait手动while循环写法解决虚假唤醒完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable std::mutex mtx; std::condition_variable cv; int i 0; void WaitFunc() { std::unique_lockstd::mutex locker(mtx); cout wait begin endl; while (i 0) { cv.wait(locker); std::this_thread::sleep_for(std::chrono::milliseconds(200)); cout wait wakeup endl; } cout -----------finished waiting .. i i endl; }核心笔记数值0代表资源暂无线程占用。 wait函数会执行两个操作自动释放互斥锁线程阻塞进入条件变量等待队列等待notify唤醒。 唤醒不等于业务条件达标唤醒仅代表线程可以参与抢锁共享变量状态不会同步更新。 线程唤醒分两种场景主动调用notify唤醒操作系统随机虚假唤醒。虚假唤醒不可信任必须用while循环二次校验条件禁止使用if判断。2.3 cv.wait lambda谓词简化写法切记谓词表达式决定线程能把不能去执行而notify_one以及notify_all只能决定线程能不能从睡眠状态中醒来醒来不代表能执行线程剩余的代码满足谓词表达式才能继续执行下去醒来不满足条件的继续进入wait的队列里面睡眠。完整代码段#includeiostream using namespace std; #includemutex #includecondition_variable std::mutex mtx; std::condition_variable cv; int i 0; void WaitLambda() { std::unique_lockstd::mutex lk(mtx); cv.wait(lk, []() { return i ! 0; }); }核心笔记线程持有锁优先执行lambda条件判断条件不满足则释放锁进入休眠线程唤醒后自动重新加锁再次执行lambda校验条件满足才跳出wait执行后续代码布尔逻辑重点区分 手动while循环 while(i0)表达式成立代表需要继续等待。 lambda表达式 return i!0返回true代表条件满足无需等待。 底层等价逻辑 while(!lambda返回值){cv.wait(lk);} 两种写法业务逻辑完全一致lambda内置循环逻辑自动规避虚假唤醒。2.4 条件变量内部两套队列底层运行机制完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable std::mutex mtx; int i 0; std::condition_variable cv; void Waits() { std::unique_lockstd::mutex locker(mtx); while (i 0) { cv.wait(locker); } }核心笔记条件变量内部维护两套独立队列条件变量等待队列执行wait的线程存放于此线程持续休眠单纯释放互斥锁无法唤醒队列内线程只能依靠notify函数。互斥锁阻塞队列抢锁失败的线程存放于此锁释放后操作系统自动唤醒队列线程竞争锁。 线程调度由操作系统全权控制。队列完整流转流程 调用cv.wait线程进入条件变量队列释放互斥锁 notify_one唤醒线程线程从条件变量队列转移至互斥锁阻塞队列排队全程保持阻塞 线程成功抢到锁后执行while或lambda条件校验条件不达标则重新回到条件变量等待队列。补充规则若执行notify_one时条件变量等待队列为空本次通知直接失效无报错、无副作用、不会留存信号。2.5 notify_one完整执行流程完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable std::mutex mtx; std::condition_variable cv; int i 0; void SignalFunc() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); cout Notifying false endl; { std::lock_guardstd::mutex locker(mtx); cout 0 endl; i 0; cv.notify_one(); } }核心笔记调用notify_one时当前线程仍持有互斥锁notify不会主动释放锁。notify仅发送唤醒信号不会改变互斥锁竞争规则被唤醒线程会尝试抢锁但锁未释放时依旧阻塞。只有lock_guard离开作用域析构释放锁后被唤醒线程才有机会竞争锁并校验条件。notify_one仅随机唤醒一个等待线程notify_all唤醒全部线程所有唤醒后的线程都需要重新竞争互斥锁。误区纠正调用notify不会实现双线程同时执行临界区代码被唤醒线程必须等待锁释放。 wait会剥夺线程锁竞争资格notify仅赋予竞争资格锁释放后线程才能正式参与抢锁。2.6 惊群现象原理与notify函数选用规则完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable std::mutex mtx; std::condition_variable cv; int i 0; void SignalAll() { std::lock_guardstd::mutex locker(mtx); i 1; cv.notify_all(); }核心笔记惊群现象定义 多个线程阻塞在同一个条件变量时notify_all会一次性唤醒全部等待线程。类比向停满鸟的树木投掷石块所有鸟类全部惊醒但仅单一线程能抢到锁执行其余线程再次休眠。notify_one等同于单独唤醒单一线程不会批量唤醒。性能损耗逻辑 全部唤醒的线程同时争抢同一互斥锁同一时刻仅单线程获取锁运行其余线程抢锁失败重新休眠大量无效线程切换消耗CPU资源。选用规范 notify_one仅唤醒单个线程无惊群损耗单任务场景优先使用。 notify_all批量唤醒所有线程触发惊群仅业务需要全部线程并发运行时使用。概念区分 虚假唤醒操作系统无通知随机唤醒单个线程。 惊群notify_all主动批量唤醒大量线程多数线程无任务执行重回休眠。2.7 sleep_for放置位置与死锁风险完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable std::mutex mtx; int i 0; void BadSleepDemo() { std::lock_guardstd::mutex locker(mtx); i 1; std::this_thread::sleep_for(std::chrono::milliseconds(400)); } void SafeSleepDemo() { std::this_thread::sleep_for(std::chrono::milliseconds(400)); std::lock_guardstd::mutex locker(mtx); i 1; }核心笔记sleep_for特性线程睡眠过程中不会释放已持有的互斥锁。 风险场景sleep_for写在锁代码块内部锁持续占用其他线程永远无法获取锁直接死锁。 安全规范延时逻辑全部放置在锁作用域外部执行。2.8 双线程奇偶有序打印边界值二次判断逻辑完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable const int n 100; int num 0; std::mutex mtx; std::condition_variable cv; void funa() { std::unique_lockstd::mutex locker(mtx); while (num n) { cv.wait(locker, []() {return num % 2 0 num n; }); if (num % 2 0 num n) { printf(funa: %d \n, num); } num 1; cv.notify_one(); } cout A endl endl; } void funb() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::unique_lockstd::mutex locker(mtx); while (num n) { while (num % 2 ! 1) { cv.wait(locker); } if (num % 2 1 num n) { printf(funb: %d \n, num); } num 1; cv.notify_one(); } cout B endl endl; }核心笔记边界二次判断必要性 当funa线程执行到边界值n并完成循环退出num数值会超出上限funb线程此时恰好被唤醒会跳过外层while循环判断直接执行打印逻辑。 增加if边界校验能够过滤超出最大值的数值避免无效打印。 运行效果funa打印偶数funb打印奇数数字有序交替输出。2.9 cv.wait_for超时等待死锁兜底方案完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable #includechrono const int n 100; int num 0; std::mutex mtx; std::condition_variable cv; void funa() { std::unique_lockstd::mutex locker(mtx); while (num n) { bool status cv.wait_for(locker, std::chrono::milliseconds(1000), []()-bool { return num % 3 0; }); if (status) { cout funa yes endl; } else { cout funa timeout endl; } } }核心笔记功能定位带超时时间的等待接口作为无超时wait无限阻塞的兜底策略杜绝程序死锁卡死。 线程释放锁进入等待队列到达设定时长后自动唤醒无需其他线程发送notify通知。lambda重载底层逻辑内置while循环notify唤醒、超时自动唤醒两种场景都会重新校验业务条件。 返回值规则true代表被通知唤醒且条件满足false代表超时自动唤醒。 超时唤醒是线程正常退出、资源回收的关键入口。2.10 多线程场景notify_one死锁问题与解决方案完整代码段#includeiostream using namespace std; #includemutex #includethread #includecondition_variable const int n 100; int num 0; std::mutex mtx; std::condition_variable cv; void funa() { std::unique_lockstd::mutex locker(mtx); while (num n) { bool status cv.wait_for(locker, std::chrono::milliseconds(1000), []()-bool { return num % 3 0; }); if (num n) cout fun a: num endl; num 1; cv.notify_all(); } cv.notify_all(); cout fun a end endl; } void funb() { std::unique_lockstd::mutex locker(mtx); while (num n) { cv.wait(locker, []()-bool { return num % 3 1; }); if (num n) cout fun b: num endl; num 1; cv.notify_all(); } cv.notify_all(); cout fun b end endl; } void func() { std::unique_lockstd::mutex locker(mtx); while (num n) { cv.wait(locker, []()-bool { return num % 3 2; }); if (num n) cout fun c: num endl; num 1; cv.notify_all(); } cv.notify_all(); cout fun c end endl; }核心笔记多线程下notify_one缺陷 三条及以上线程同时阻塞在条件变量队列仅单线程处于运行状态时调用notify_one只会随机唤醒一条线程。若唤醒后的线程不满足运行条件会重新进入休眠队列所有线程全部休眠程序卡死死锁。两种解决方式 方案一统一使用notify_all一次性唤醒全部等待线程。 方案二全部使用wait_for超时等待超时自动唤醒避免永久阻塞。 运行分配规则funa打印3的倍数funb打印除3余1数字func打印除3余2数字。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门