【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 类里。构造函数里自动完成初始化析构函数里自动完成销毁。这就变成了只要这个类对象创建了条件变量就活了对象出了作用域被销毁了条件变量也就自动清理了。同时你不需要再手动去操心初始化和销毁代码放在哪了。

相关新闻

如何在5分钟内为Audacity免费添加AI音频处理能力:OpenVINO插件完整指南

如何在5分钟内为Audacity免费添加AI音频处理能力:OpenVINO插件完整指南

如何在5分钟内为Audacity免费添加AI音频处理能力:OpenVINO插件完整指南 【免费下载链接】openvino-plugins-ai-audacity A set of AI-enabled effects, generators, and analyzers for Audacity. 项目地址: https://gitcode.com/gh_mirrors/op/openvino-plugins-a…

2026/10/7 10:07:07 阅读更多 →
如何5分钟配置《绝区零》智能游戏助手:新手快速上手终极指南

如何5分钟配置《绝区零》智能游戏助手:新手快速上手终极指南

如何5分钟配置《绝区零》智能游戏助手:新手快速上手终极指南 【免费下载链接】ZenlessZoneZero-OneDragon 绝区零 一条龙 | 全自动 | 自动闪避 | 自动每日 | 自动空洞 | 支持手柄 项目地址: https://gitcode.com/gh_mirrors/ze/ZenlessZoneZero-OneDragon 还…

2026/10/9 22:50:52 阅读更多 →
抖音下载器V2.0:3个实战技巧帮你高效提取1080P高清封面

抖音下载器V2.0:3个实战技巧帮你高效提取1080P高清封面

抖音下载器V2.0:3个实战技巧帮你高效提取1080P高清封面 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback supp…

2026/10/13 18:58:00 阅读更多 →

最新新闻

TS流PCAP分析实战:从网络抓包定位音视频卡顿根源

TS流PCAP分析实战:从网络抓包定位音视频卡顿根源

1. 项目概述:为什么你手里的TS流PCAP文件比想象中更有价值“mpeg ts pcap 文件分析”——这串词看起来像一串技术黑话,但背后藏着大量真实业务场景里被忽视的关键信息。我接触过不少网络运维、音视频开发、IoT设备调试甚至安全分析一线的同行&#xff0c…

2026/10/14 6:05:23 阅读更多 →
C语言链表排序实战:插入排序与快速排序的指针操作详解

C语言链表排序实战:插入排序与快速排序的指针操作详解

简介:链表排序是C语言学习者绕不开的实践主题,这份源码压缩包面向正在掌握指针操作、动态内存分配和基础算法的初学者,帮助理解链表节点结构与排序算法的结合方式,也适合作为数据结构课程实验后的巩固练习。包体十分精简&#xff…

2026/10/14 6:05:23 阅读更多 →
从流量焦虑到AI获客:GEO优化师2026年实战复盘

从流量焦虑到AI获客:GEO优化师2026年实战复盘

1. 从流量焦虑到AI获客:一个优化师的2026年实战复盘过去这一年,我几乎每周都要跟不同行业的企业负责人聊同一个话题:钱花得越来越多,线索却越来越水。SEM账户里点击单价翻了一倍,信息流广告的转化率跌到千分之三以下&a…

2026/10/14 6:05:23 阅读更多 →
React Native鸿蒙跨平台固定表头长列表表格:双区布局与性能优化

React Native鸿蒙跨平台固定表头长列表表格:双区布局与性能优化

1. 一个被数据压垮的真实场景:长列表表格的"阅读灾难"上个月我接手了一个移动端报表类项目的数据展示模块,技术栈是 React Native,目标是跑通鸿蒙平台。先说结论:这个模块在改造之前,几乎处于"能看但没…

2026/10/14 6:05:23 阅读更多 →
Preliminary规格书阅读指南:锁参数、避坑、做版本跟踪

Preliminary规格书阅读指南:锁参数、避坑、做版本跟踪

简介:《PLA5506-940》是飞利浦光电提供的一份初步机密级产品规格书,版本V0.8,文档编号992215900750,适合光电与激光雷达、光通信、精密传感等领域的硬件工程师、选型评估人员和采购团队参考。该PDF围绕PLA5506-940型VCSEL阵列展开…

2026/10/14 6:05:23 阅读更多 →
一条富碱性多肽的进阶之路:序列解析、合成纯化与核定位验证

一条富碱性多肽的进阶之路:序列解析、合成纯化与核定位验证

1. 从一条氨基酸序列反推蛋白身份:这条肽的“户口本”是这样查的拿到 PDGF A-Chain (194-211) 这个编号,加上后面那一串 GRPREGSKKRKRKRLKPT,我的第一反应不是急着做实验,而是先把它当成一个“嫌疑人”来审:它到底来自…

2026/10/14 6:04:23 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →