ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 线程同步:条件变量

Linux 线程同步:条件变量 Linux 线程同步条件变量课程尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理依据2026-09-29 19:57 录音转写整理到15:02课程 PDF 第 164 页起。页码说明第 164 页是自旋锁收尾和条件变量入口pthread_cond_*接口及生产者消费者示例实际延续到第 165—168 页。录音 15:05 后的运行结果和后续改写不在本节范围内。1. 录音开头的自旋锁收尾录音 00:02—01:15 先结束上一部分的自旋锁介绍。互斥锁获取失败时线程通常进入阻塞状态自旋锁获取失败时执行单元持续循环检查锁是否释放。互斥锁拿不到 → 阻塞等待 → 被调度器重新唤醒 自旋锁拿不到 → 循环检查 → 一旦释放立即尝试获取自旋锁适合临界区非常短、预计等待时间很短、上下文切换成本相对不划算的场景。等待时间一长它会持续占用 CPU。Linux 内核广泛使用自旋锁。教材第 164 页说“不能在用户空间使用”过于绝对POSIX/Linux 提供了pthread_spin_*用户态接口但普通应用很少需要它。当前应用层课程优先掌握互斥锁和条件变量即可。2. 条件变量要解决什么问题互斥锁能回答现在谁可以进入临界区它不能直接回答缓冲区什么时候有数据什么时候有空位任务什么时候完成如果线程只是不断加锁、检查、解锁会形成忙等或低效轮询for(;;){pthread_mutex_lock(mutex);if(count0){/* 消费数据 */pthread_mutex_unlock(mutex);break;}pthread_mutex_unlock(mutex);}条件变量允许线程在条件不满足时睡眠并在睡眠前释放互斥锁让其他线程能够修改共享状态。状态改变后其他线程再通知等待者重新检查条件。3. 条件变量不是锁也不保存业务状态条件变量pthread_cond_t不负责保护共享数据也不等于一个布尔变量。实际条件来自共享状态count0/* 缓冲区为空消费者不能继续 */countBUFFER_SIZE/* 缓冲区已满生产者不能继续 */task_finished/* 某项任务已经完成 */三者分工如下对象作用共享状态描述条件当前是否成立例如count互斥锁保护共享状态的检查和修改条件变量让线程睡眠并在状态可能改变时接收通知条件变量不记住历史通知。如果调用pthread_cond_signal()时没有线程正在等待这次通知不会保存到以后。4.pthread_cond_wait()的完整动作调用前线程必须已经持有配套的互斥锁pthread_mutex_lock(mutex);while(count0){pthread_cond_wait(not_empty,mutex);}/* 返回时已经重新持有 mutex */consume_data();pthread_mutex_unlock(mutex);pthread_cond_wait()内部完成一组关键动作否是线程已经持有 mutex条件是否成立pthread_cond_wait原子地释放 mutex 并进入等待其他线程取得 mutex修改共享状态signal 或 broadcast等待线程被唤醒重新竞争并取得 mutex操作共享数据释放 mutex这里的“原子地释放并等待”非常重要。如果释放锁和进入等待分成两个互不关联的步骤通知可能恰好发生在两步之间导致线程错过通知并永久等待。5. 为什么必须用while不能只用if正确模板pthread_mutex_lock(mutex);while(!condition_is_true()){pthread_cond_wait(cond,mutex);}use_shared_state();pthread_mutex_unlock(mutex);等待返回只表示“线程获得了重新检查条件的机会”不保证条件此刻仍然成立原因包括可能发生伪唤醒broadcast会同时唤醒多个线程某个线程先获得互斥锁并消耗了资源状态在当前线程重新取得锁之前再次改变。因此必须在重新持有互斥锁后再次检查谓词。不成立就继续等待。错误写法if(count0){pthread_cond_wait(cond,mutex);}/* 醒来后不检查就直接读取可能出错 */6.signal与broadcastintpthread_cond_signal(pthread_cond_t*cond);intpthread_cond_broadcast(pthread_cond_t*cond);操作效果常见用途pthread_cond_signal至少唤醒一个等待者新增一个资源只需一个线程处理pthread_cond_broadcast唤醒所有当前等待者状态变化后可能允许多个线程继续被唤醒不等于马上执行。等待线程从pthread_cond_wait()返回前还要重新获得与等待绑定的互斥锁。如果没有线程等待signal和broadcast都不会产生可供以后使用的“通知余额”。7. 条件变量的初始化与销毁7.1 静态初始化staticpthread_cond_tcondPTHREAD_COND_INITIALIZER;staticpthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;默认属性下这种写法最简单。7.2 动态初始化pthread_cond_tcond;interrorpthread_cond_init(cond,NULL);第二个参数为NULL表示默认属性。7.3 销毁interrorpthread_cond_destroy(cond);销毁时必须确认没有线程仍在该条件变量上等待也不会再有线程访问它。8. 本节核心 APIintpthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex);intpthread_cond_timedwait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex,conststructtimespec*restrict abstime);intpthread_cond_signal(pthread_cond_t*cond);intpthread_cond_broadcast(pthread_cond_t*cond);8.1 定时等待pthread_cond_timedwait()增加一个绝对截止时间。到期时返回ETIMEDOUT返回前同样会重新取得互斥锁。#includeerrno.h#includetime.hstructtimespecdeadline;clock_gettime(CLOCK_REALTIME,deadline);deadline.tv_sec2;while(count0){interrorpthread_cond_timedwait(cond,mutex,deadline);if(errorETIMEDOUT){break;}}注意第三个参数通常是绝对时间不是“等待多少秒”的相对时长。9.restrict关键字怎样理解教材第 164 页从restrict引出条件变量接口。它是 C99 的指针限定符向编译器承诺在相应作用域内对相关对象的访问遵守特定的无别名规则以便编译器优化。初学阶段需要掌握它主要是程序员对编译器作出的约定违反约定可能导致未定义行为它不是运行时检查看到函数原型中的restrict不需要自行删除或改写。把它简单说成“内存中只能存在一个指针指向该对象”并不严谨。重点是特定作用域内通过哪些指针访问对象而不是物理上绝不允许其他指针值存在。10. 录音中的生产者消费者模型录音从 06:46 开始建立一个长度为 5 的缓冲区#defineBUFFER_SIZE5staticintbuffer[BUFFER_SIZE];staticintcount0;staticpthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;staticpthread_cond_tcondPTHREAD_COND_INITIALIZER;生产者逻辑加锁 → 缓冲区满时等待 → 写入一个数据并增加 count → 通知消费者 → 解锁消费者逻辑加锁 → 缓冲区空时等待 → 读取一个数据并减少 count → 通知生产者 → 解锁录音到 15:02 已写完这套基本逻辑但还没有进入运行结果分析。11. 简化练习单槽生产者消费者为了先练懂等待流程下面把长度为 5 的数组简化成一个槽位并让程序生产、消费 5 次后正常退出。#includepthread.h#includestdio.h#includestdlib.h#includestring.h#defineITEM_COUNT5staticintslot;staticinthas_item0;staticpthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;staticpthread_cond_tcan_producePTHREAD_COND_INITIALIZER;staticpthread_cond_tcan_consumePTHREAD_COND_INITIALIZER;staticvoidcheck_pthread(interror,constchar*operation){if(error!0){fprintf(stderr,%s: %s\n,operation,strerror(error));exit(EXIT_FAILURE);}}staticvoid*producer(void*arg){(void)arg;for(intvalue1;valueITEM_COUNT;value){check_pthread(pthread_mutex_lock(mutex),pthread_mutex_lock);while(has_item){check_pthread(pthread_cond_wait(can_produce,mutex),pthread_cond_wait);}slotvalue;has_item1;printf(生产%d\n,value);check_pthread(pthread_cond_signal(can_consume),pthread_cond_signal);check_pthread(pthread_mutex_unlock(mutex),pthread_mutex_unlock);}returnNULL;}staticvoid*consumer(void*arg){(void)arg;for(inti0;iITEM_COUNT;i){check_pthread(pthread_mutex_lock(mutex),pthread_mutex_lock);while(!has_item){check_pthread(pthread_cond_wait(can_consume,mutex),pthread_cond_wait);}intvalueslot;has_item0;printf(消费%d\n,value);check_pthread(pthread_cond_signal(can_produce),pthread_cond_signal);check_pthread(pthread_mutex_unlock(mutex),pthread_mutex_unlock);}returnNULL;}intmain(void){pthread_tproducer_id;pthread_tconsumer_id;check_pthread(pthread_create(producer_id,NULL,producer,NULL),pthread_create);check_pthread(pthread_create(consumer_id,NULL,consumer,NULL),pthread_create);check_pthread(pthread_join(producer_id,NULL),pthread_join);check_pthread(pthread_join(consumer_id,NULL),pthread_join);check_pthread(pthread_cond_destroy(can_produce),pthread_cond_destroy);check_pthread(pthread_cond_destroy(can_consume),pthread_cond_destroy);check_pthread(pthread_mutex_destroy(mutex),pthread_mutex_destroy);returnEXIT_SUCCESS;}编译运行gcc-Wall-Wextra-pthreadcond_single_slot.c-ocond_single_slot ./cond_single_slot预期得到交替输出生产1 消费1 生产2 消费2 ...11.1 为什么示例使用两个条件变量录音的一生产者、一消费者示例使用一把互斥锁和一个条件变量也能表达基本过程。练习代码拆成两个条件变量can_produce有空位可以生产 can_consume有数据可以消费这样通知目标更清楚也更容易扩展为多个生产者和消费者。互斥锁仍然只有一把因为两个条件都由同一组共享状态slot has_item决定。12. 通知与解锁的顺序常见且易理解的写法是pthread_mutex_lock(mutex);change_shared_state();pthread_cond_signal(cond);pthread_mutex_unlock(mutex);核心要求是检查和修改谓词时持有互斥锁通知对应的是已经完成的状态变化等待者醒来后仍通过同一把锁重新检查状态。POSIX 允许在不持有互斥锁时调用signal但初学阶段应保持统一模式减少通知与状态变化脱节的风险。13. 常见错误错误后果正确做法调用wait前没有持有互斥锁错误或未定义行为先pthread_mutex_lock用if检查条件醒来后条件可能已失效使用while反复检查把条件变量当成状态无法判断能否继续用共享变量保存谓词认为signal会累计先通知后等待时丢失通知始终检查受锁保护的状态修改共享状态时不加锁产生数据竞争状态检查和修改使用同一把锁wait返回后再次手动加锁重复加锁可能死锁wait返回时已经持锁在持锁时做长时间无关工作其他线程无法修改条件缩短临界区有等待线程时销毁条件变量未定义行为先结束并回收所有线程把sleep()当作通知执行顺序不可靠使用条件变量通知状态变化14. 录音与教材表述校正转写中的“自选锁”应为“自旋锁spinlock”。用户空间并非绝对不能使用自旋锁Linux 提供pthread_spin_*只是普通应用通常不适合使用。条件变量不是“暂时把条件释放”而是等待线程原子地释放互斥锁并阻塞。pthread_cond_wait()返回之前已经重新取得互斥锁。被通知只代表条件可能成立必须使用while再次检查共享谓词。pthread_cond_signal()唤醒至少一个等待线程具体哪个线程由调度决定不能理解为固定或真正的随机选择。pthread_cond_timedwait()使用绝对截止时间并在超时返回前重新取得互斥锁。restrict不是“世界上只能存在一个指针”而是关于特定作用域中访问路径的编译优化约定。条件变量没有存储通知的计数。没有等待者时发送通知不会留给未来线程。条件变量可以降低无效竞争和轮询但不能笼统称为“完全无竞争”被唤醒的线程仍要竞争互斥锁。15. 本节最低掌握标准学完后应能回答条件变量与互斥锁分别负责什么pthread_cond_wait()调用前和返回后互斥锁分别是什么状态为什么条件判断必须写成while条件变量会不会保存一次无人接收的signalsignal和broadcast有什么区别生产者发现缓冲区满时怎样等待消费者发现缓冲区空时怎样等待为什么修改count或has_item时仍然必须加锁最低实践要求能默写“加锁 → while 检查 → wait → 操作状态 → signal → 解锁”能解释wait为什么不会造成其他线程永远拿不到互斥锁能运行并修改第 11 节的单槽生产者消费者程序能把while改成if后指出潜在错误而不是只观察一次运行结果。16. PDF 页码与录音时间索引主题PDF 页码录音时间自旋锁收尾16400:02—01:15条件变量用途164 起01:17—01:55restrict关键字164—16501:56—03:21wait、定时等待、通知与广播165—16603:22—05:52条件变量类型与初始化166—16805:53—06:34、09:21—10:15生产者消费者代码搭建168 页起06:46—15:02程序运行和后续改写后续页15:05 以后本次不整理进一步核对资料POSIX Programmer’s Manual条件等待与定时等待POSIX Programmer’s Manual单个通知与广播POSIX Programmer’s Manual条件变量初始化与销毁Linux man-pages用户态 Pthreads 自旋锁17. 下一步学习本次截止在代码写完、准备运行的位置。下一段应继续观察一个条件变量时生产者和消费者的实际交错顺序为什么改变互斥锁的持有范围会改变“一次生产几个、一次消费几个”怎样正确安排通知位置避免双方都进入等待多生产者、多消费者时为什么通常拆成not_full与not_empty两个条件变量。
返回列表