发布时间:2026/8/5 0:06:34
【Linux】二十八.线程篇五《Linux多线程编程:线程同步之条件变量》---详解 在上节内容中我们学习了线程篇五这节内容我们详细讲解线程同步.线程同步这是上节内容我们描写的是模拟抢票系统先看这段经典的多线程抢票逻辑虽然我们给它加上了互斥锁来保护数据但这就像给自习室门口安了一把只能单人进出的锁依然治不了“抢票狂魔”的问题——那个离锁最近、手速最快的线程总是能抢在别人前面一遍遍地把票扫光导致其他线程连汤都喝不上只能饿着肚子空转。这种现象在计算机里叫“饥饿问题”这个在上节内容中给大家讲过它本身没有语法错误但极其不合理就好比钥匙刚从墙上的挂钩摘下还没来得及给别人用同一个学生又顺手拿了进去反复循环只图自己方便。实际上真正合理的做法必须加上一条“排队”规则任何人用完钥匙挂回墙上后不能立刻再申请必须老老实实去队伍后面排队。把这个“按顺序办事”的规则落实到代码里来保证所有人都能公平地获取资源就是我们要引入的核心概念——线程同步。排队的本质就是在互斥保证安全的基础上赋予线程一个公平的申请与释放顺序这就是线程同步。它解决的不是数据对不对而是分配合理不合理的问题。正因如此互斥和同步总是成对出现光有互斥始终无法保证临界资源得到有序的合理访问。要说线程同步我们就要引入条件变量1.条件变量概念条件变量本质上是原生线程库提供的一个同步机制。它不是一个用来存数据的普通变量而是一个用来描述临界资源当前状态的信号器。它的作用类似于消息通知让一个线程可以挂起等待直到另一个线程改变了共享资源的状态并通过条件变量发出资源就绪的信号来唤醒它。我们在写抢票程序时很容易陷入一个误区如果发现票卖完了线程还是会拼命去申请锁、检查、再释放锁循环往复这个是错误的而是不应该再申请锁了而应该等有票了再申请。这就像中午你想去饮水机接水走过去一看桶空了你走开了过了两秒你又跑过去看又空了再走开……反复折腾。逻辑上你确实没做错但这种“轮询”不仅浪费了你的腿脚CPU计算资源还让本来想接水的人总要被你挡一下锁的争抢加剧。实际上更合理的做法是既然是没水了你就应该在饮水机旁边贴张纸条告诉送水工水来了叫我一声。然后你在旁边安心休息等送水工换好新桶敲敲你的肩膀唤醒你你再去接水。这里的“纸条”或“唤醒信号”就是用来描述临界资源状态水有没有的关键也就是我们要引入的条件变量。之所以之前我们一直在盲目地轮询申请锁根本原因就是线程之间缺乏沟通大家完全不清楚临界资源当前的状况只能靠撞大运般地去查。在张图左边负责“放苹果”右边负责“拿苹果”中间有个盘子临界资源。首先我们要明白放和拿的动作是连续的如果不加控制就会出现二义性——万一我这边还在放你那边就来拿了谁说得准这苹果到底算不算已经放好了所以锁的作用就是强制规定不管你是放还是拿必须先拿钥匙加锁把整个过程变成不可分割的“原子操作”保证盘子里的状态是明确的。但光有锁还不够。右边有个典型的轮询陷阱放苹果的人手脚麻利拿苹果的人磨磨蹭蹭。如果两人都是瞎子互相不知道对方动作放苹果的人就会反复加锁、一看苹果还在、解锁接着马上又加锁……他反反复复地瞎忙活虽然代码没错但极其浪费CPU而行动慢的拿苹果的人连抢到锁的机会都没有陷入了“饥饿”。这时图里红框标出的“铃铛”条件变量和旁边的“队列”就派上用场了。合理的做法是放苹果的人加好锁、放下苹果解锁后立刻敲一下铃铛然后去旁边挂起睡觉。这个铃铛的作用就是描述当前盘子临界资源有没有苹果的状态。另一边拿苹果的人并不是盲目地去抢锁而是先在队列等待队列里好好排队等着。一旦听到铃铛响他知道有苹果了就会从队列里被唤醒从容地去加锁、拿走苹果并敲铃铛通知对方。当⼀个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如⼀个线程访问队列时发现队列为空它只能等待只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量。2.同步概念与竞态条件同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从⽽有效避免饥饿问题叫做同步竞态条件因为时序问题⽽导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解详细内容上一节我都讲过了3.条件变量函数函数接口介绍初始化int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);参数cond要初始化的条件变量attrNULL销毁int pthread_cond_destroy(pthread_cond_t *cond)等待条件满足pthread_cond_wait(cond, mutex)使调用线程在指定条件变量cond上阻塞并挂入其等待队列。关键机制阻塞挂起的瞬间系统会原子性地释放互斥锁mutex避免死锁当被唤醒后系统又会原子性地重新获取该锁之后线程才得以继续执行后文生产者消费者模型将详细拆解。pthread_cond_timedwait(cond, mutex, abstime)接口与wait完全一致仅多出超时控制参数abstime绝对时间。若指定时间内未收到唤醒信号线程将自动解除阻塞。主要用于规避无限等待的风险实际应用场景相对有限。参数cond要在这个条件变量上等待mutex互斥量后⾯详细解释唤醒等待pthread_cond_signal(cond)唤醒在指定条件变量cond的等待队列中阻塞的第一个线程。常用于资源仅能满足单个消费者的情况。pthread_cond_broadcast(cond)唤醒所有当前在该条件变量cond下阻塞等待的线程。多用于资源状态发生重大变化如一次性释放了多个资源需要通知所有等待者重新竞争应用场景相对较少。4.为什么 pthread_ cond_ wait 需要互斥量?条件等待是线程间同步的⼀种⼿段如果只有⼀个线程条件不满⾜⼀直等下去都不会满⾜所以必须要有⼀个线程通过某些操作改变共享变量使原先不满⾜的条件变得满⾜并且友好的通知等待在条件变量上的线程。条件不会⽆缘⽆故的突然变得满⾜了必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。按照上⾯的说法我们设计出如下的代码先上锁发现条件不满⾜解锁然后等待在条件变量上不就⾏了如下代码:// 错误示范存在致命的信号丢失风险 pthread_mutex_lock(mutex); while (condition_is_false) { pthread_mutex_unlock(mutex); // 步骤1主动解锁 //解锁之后等待之前条件可能已经满足信号已经发出但是该信号可能被错过 pthread_cond_wait(cond); // 步骤2此时才开始去排队等待 pthread_mutex_lock(mutex); // 步骤3醒来后自己抢锁 } pthread_mutex_unlock(mutex);由于解锁和等待不是原⼦操作。调⽤解锁之后 pthread_cond_wait 之前如果已经有其他线程获取到互斥量摒弃条件满⾜发送了信号那么 pthread_cond_wait 将错过这个信号可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是⼀个原⼦操作。int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t *mutex); 进⼊该函数后会去看条件量等于0不等于就把互斥量变成1直到cond_ wait返回把条件量改成1把互斥量恢复成原样。5.条件变量使用规范1.等待条件代码pthread_mutex_lock(mutex); // 必须使用 while 循环而不是 if来防止假的唤醒 while (条件为假) { pthread_cond_wait(cond, mutex); } // 此时条件为真可以安全地操作共享资源 修改条件; pthread_mutex_unlock(mutex);在这里为什么while是必须的因为即使被signal唤醒了也可能因为系统调度或竞争导致共享变量状态在被唤醒的瞬间又被其他线程抢走了。使用while可以确保线程醒来后重新检查一次如果条件又不满足了就继续回去等。给条件发送信号代码pthread_mutex_lock(mutex); // 修改共享变量使等待的条件变为真 设置条件为真; // 发出信号唤醒在 cond 上等待的一个线程 pthread_cond_signal(cond); pthread_mutex_unlock(mutex);修改条件和发送信号必须在同一个互斥锁的保护下完成。这样可以保证通知者写数据时等待者不会同时读数据数据一致且信号不会被错过。代码如下#include iostream #include string #include unistd.h #include pthread.h #define TNUM 4 typedef void (*func_t)(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond); volatile bool quit false; // pthread_cond_t cond PTHREAD_COND_INITIALIZER; // pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER; class ThreadData { public: ThreadData(const std::string name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond) :name_(name), func_(func), pmtx_(pmtx), pcond_(pcond) {} public: std::string name_; func_t func_; pthread_mutex_t *pmtx_; pthread_cond_t *pcond_; }; void func1(const std::string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { // wait一定要在加锁和解锁之间进行wait pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); //默认该线程在执行时wait代码被执行当前线程会被立即被阻塞 std::cout name running -- 播放 std::endl; pthread_mutex_unlock(pmtx); } } void func2(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); if(!quit) std::cout name running -- 下载 std::endl; pthread_mutex_unlock(pmtx); } } void func3(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); std::cout name running -- 刷新 std::endl; pthread_mutex_unlock(pmtx); } } void func4(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); std::cout name running -- 扫码用户信息 std::endl; pthread_mutex_unlock(pmtx); } } void *Entry(void *args) { ThreadData *td (ThreadData*)args; // td在每一个线程自己私有的栈空间中保存 td-func_(td-name_, td-pmtx_, td-pcond_); // 它是一个函数调用完成就要返回 delete td; return nullptr; } int main() { pthread_mutex_t mtx; pthread_cond_t cond; pthread_mutex_init(mtx, nullptr); pthread_cond_init(cond, nullptr); pthread_t tids[TNUM]; func_t funcs[TNUM] {func1, func2, func3, func4}; for (int i 0; i TNUM; i) { std::string name Thread ; name std::to_string(i1); ThreadData *td new ThreadData(name, funcs[i], mtx, cond); pthread_create(tids i, nullptr, Entry, (void*)td); } sleep(5); // ctrl new thread int cnt 10; while(cnt) { std::cout resume thread run code .... cnt-- std::endl; pthread_cond_signal(cond); // pthread_cond_broadcast(cond); sleep(1); } std::cout ctrl done std::endl; quit true; pthread_cond_broadcast(cond); for(int i 0; i TNUM; i) { pthread_join(tids[i], nullptr); std::cout thread: tids[i] quit std::endl; } pthread_mutex_destroy(mtx); pthread_cond_destroy(cond); return 0; }这段代码主要是利用互斥锁和条件变量实现线程的休眠与唤醒控制。程序启动之后主线程先创建四个子线程各个子线程运行后就调用 pthread_cond_wait 进入阻塞等待状态此时线程就挂在条件变量的等待队列上。主线程等待几秒之后循环调用 pthread_cond_signal 每次唤醒一个子线程被唤醒的线程拿到锁就执行对应的业务打印执行完解锁后又会再次进入等待。等唤醒循环结束把全局退出标记 quit 置为 true再通过 pthread_cond_broadcast 唤醒所有线程让各个子线程跳出循环正常结束最后主线程 join 回收线程资源销毁锁与条件变量完成整个程序的收尾。6. 条件变量的封装基于上⾯的基本认识我们已经知道条件变量如何使⽤虽然细节需要后⾯再来进⾏解释但这⾥可以做⼀下基本的封装#pragma once #include iostream #include string #include pthread.h #include Lock.hpp namespace CondModule { using namespace LockModule; class Cond { public: Cond() { int n pthread_cond_init(_cond, nullptr); (void)n; // 实际工程中此处应做 n ! 0 的异常判断或日志记录 } void Wait(Mutex *mutex) { // 传入底层互斥锁的指针 int n pthread_cond_wait(_cond, mutex-GetMutexOriginal()); (void)n; } void Notify() { int n pthread_cond_signal(_cond); (void)n; } void NotifyAll() { int n pthread_cond_broadcast(_cond); (void)n; } ~Cond() { int n pthread_cond_destroy(_cond); (void)n; // 实际工程中此处应做 n ! 0 的异常判断或日志记录 } private: pthread_cond_t _cond; }; }原来的 C 语言做法你必须在主函数里声明pthread_cond_t cond;然后调用pthread_cond_init()初始化最后程序结束前还得记得调用pthread_cond_destroy()销毁。一旦忘了销毁就会造成内存泄露。这个封装的思路把底层的 C 变量pthread_cond_t _cond包进 C 类里。构造函数里自动完成初始化析构函数里自动完成销毁。这就变成了只要这个类对象创建了条件变量就活了对象出了作用域被销毁了条件变量也就自动清理了。同时你不需要再手动去操心初始化和销毁代码放在哪了。

相关新闻

2026/8/5 1:06:42

2026瓷砖一线品牌金丝玉玛:高端时尚带点金,演绎家居美学新风尚

在当下家居装修市场中,消费者对瓷砖的选择早已不再局限于基础的功能性需求,而是更加注重品质感、设计感与品牌调性的融合。瓷砖一线品牌作为行业中的标杆力量,承载着消费者对高品质家居生活的期待。其中,金丝玉玛瓷砖以"高端…

2026/8/5 1:06:42

高端陶瓷十大品牌怎么选:金丝玉玛,中国高端瓷砖品牌开创者

在中国陶瓷行业的发展历程中,有这样一类品牌:它们不满足于常规产品的制造,而是选择在细分领域深耕细作,以差异化策略开辟全新的市场空间。金丝玉玛瓷砖便是其中的典型代表。作为高端陶瓷十大品牌之一,金丝玉玛自2006年…

2026/8/5 1:06:42

NsEmuTools完整指南:3步轻松管理NS模拟器的终极解决方案

NsEmuTools完整指南:3步轻松管理NS模拟器的终极解决方案 【免费下载链接】ns-emu-tools 一个用于安装/更新 NS 模拟器的工具 项目地址: https://gitcode.com/gh_mirrors/ns/ns-emu-tools 还在为任天堂Switch模拟器的复杂安装和配置而烦恼吗?NsEmu…

2026/8/5 1:01:41

3大技术创新:FontCenter如何重塑AutoCAD字体管理生态

3大技术创新:FontCenter如何重塑AutoCAD字体管理生态 【免费下载链接】FontCenter AutoCAD自动管理字体插件 项目地址: https://gitcode.com/gh_mirrors/fo/FontCenter AutoCAD字体缺失问题长期困扰着工程设计行业,FontCenter项目通过创新的客户端…

2026/8/3 21:14:30

如何用免费工具突破游戏窗口限制:SRWE完整使用指南

如何用免费工具突破游戏窗口限制:SRWE完整使用指南 【免费下载链接】SRWE Simple Runtime Window Editor 项目地址: https://gitcode.com/gh_mirrors/sr/SRWE 你是否遇到过这样的困扰?想为心爱的游戏截图,却发现游戏不支持自定义分辨率…

2026/8/5 0:01:34

三升四,比成绩下滑更可怕的,是孩子开始「认命」

分水岭上,最难的不是翻过去,是孩子不想翻了。八月初了。这两个字,对三升四的家长来说,比任何闹钟都让人清醒。最近的家长群里,气氛明显不一样了。一升二的在关心兴趣班,二升三的在讨论要不要提前学英语。而…

2026/8/5 0:01:34

Java缓存框架:JetCache

TOC 一、简介 JetCache 是一个 Java 缓存抽象框架,为不同的缓存解决方案提供了统一的使用方式。 它提供的注解比 Spring Cache 更加强大。 JetCache 的注解支持原生 TTL、两级缓存以及在分布式环境中的自动刷新功能,同时你也可以通过代码直接操作 Cach…

2026/8/5 0:01:34

AD 铺铜设置十字连接,过孔全连接,新版AD的简单设置

需求:通孔焊盘 十字花;过孔 Via 实心直连;贴片焊盘按需设置 AD 测试版本AD24 很多工程师踩坑:全部统一十字,导致接地过孔阻抗高、大电流发热! 一、快捷键打开规则 PCB 界面按下:D R 展开…

2026/8/3 22:40:58

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/3 13:26:41

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/3 16:43:13

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…