Linux 线程同步:条件变量
课程:尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理
依据:2026-09-29 19:57 录音转写,整理到 15:02 ;课程 PDF 第 164 页起。
页码说明:第 164 页是自旋锁收尾和条件变量入口;
pthread_cond_*接口及生产者消费者示例实际延续到第 165---168 页。录音 15:05 后的运行结果和后续改写不在本节范围内。
1. 录音开头的自旋锁收尾
录音 00:02---01:15 先结束上一部分的自旋锁介绍。
互斥锁获取失败时,线程通常进入阻塞状态;自旋锁获取失败时,执行单元持续循环检查锁是否释放。
text
互斥锁:拿不到 → 阻塞等待 → 被调度器重新唤醒
自旋锁:拿不到 → 循环检查 → 一旦释放立即尝试获取
自旋锁适合临界区非常短、预计等待时间很短、上下文切换成本相对不划算的场景。等待时间一长,它会持续占用 CPU。
Linux 内核广泛使用自旋锁。教材第 164 页说"不能在用户空间使用"过于绝对:POSIX/Linux 提供了 pthread_spin_* 用户态接口,但普通应用很少需要它。当前应用层课程优先掌握互斥锁和条件变量即可。
2. 条件变量要解决什么问题
互斥锁能回答:
现在谁可以进入临界区?
它不能直接回答:
缓冲区什么时候有数据?什么时候有空位?任务什么时候完成?
如果线程只是不断加锁、检查、解锁,会形成忙等或低效轮询:
c
for (;;) {
pthread_mutex_lock(&mutex);
if (count > 0) {
/* 消费数据 */
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
条件变量允许线程在条件不满足时睡眠,并在睡眠前释放互斥锁,让其他线程能够修改共享状态。状态改变后,其他线程再通知等待者重新检查条件。
3. 条件变量不是锁,也不保存业务状态
条件变量 pthread_cond_t 不负责保护共享数据,也不等于一个布尔变量。
实际条件来自共享状态:
c
count == 0 /* 缓冲区为空,消费者不能继续 */
count == BUFFER_SIZE /* 缓冲区已满,生产者不能继续 */
task_finished /* 某项任务已经完成 */
三者分工如下:
| 对象 | 作用 |
|---|---|
| 共享状态 | 描述条件当前是否成立,例如 count |
| 互斥锁 | 保护共享状态的检查和修改 |
| 条件变量 | 让线程睡眠,并在状态可能改变时接收通知 |
条件变量不记住历史通知。如果调用 pthread_cond_signal() 时没有线程正在等待,这次通知不会保存到以后。
4. pthread_cond_wait() 的完整动作
调用前,线程必须已经持有配套的互斥锁:
c
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
/* 返回时已经重新持有 mutex */
consume_data();
pthread_mutex_unlock(&mutex);
pthread_cond_wait() 内部完成一组关键动作:
#mermaid-svg-fFdaaBB1TY8Js3h9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fFdaaBB1TY8Js3h9 .error-icon{fill:#552222;}#mermaid-svg-fFdaaBB1TY8Js3h9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fFdaaBB1TY8Js3h9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .marker.cross{stroke:#333333;}#mermaid-svg-fFdaaBB1TY8Js3h9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fFdaaBB1TY8Js3h9 p{margin:0;}#mermaid-svg-fFdaaBB1TY8Js3h9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster-label text{fill:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster-label span{color:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster-label span p{background-color:transparent;}#mermaid-svg-fFdaaBB1TY8Js3h9 .label text,#mermaid-svg-fFdaaBB1TY8Js3h9 span{fill:#333;color:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .node rect,#mermaid-svg-fFdaaBB1TY8Js3h9 .node circle,#mermaid-svg-fFdaaBB1TY8Js3h9 .node ellipse,#mermaid-svg-fFdaaBB1TY8Js3h9 .node polygon,#mermaid-svg-fFdaaBB1TY8Js3h9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .rough-node .label text,#mermaid-svg-fFdaaBB1TY8Js3h9 .node .label text,#mermaid-svg-fFdaaBB1TY8Js3h9 .image-shape .label,#mermaid-svg-fFdaaBB1TY8Js3h9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-fFdaaBB1TY8Js3h9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .rough-node .label,#mermaid-svg-fFdaaBB1TY8Js3h9 .node .label,#mermaid-svg-fFdaaBB1TY8Js3h9 .image-shape .label,#mermaid-svg-fFdaaBB1TY8Js3h9 .icon-shape .label{text-align:center;}#mermaid-svg-fFdaaBB1TY8Js3h9 .node.clickable{cursor:pointer;}#mermaid-svg-fFdaaBB1TY8Js3h9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .arrowheadPath{fill:#333333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fFdaaBB1TY8Js3h9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fFdaaBB1TY8Js3h9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fFdaaBB1TY8Js3h9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster text{fill:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 .cluster span{color:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fFdaaBB1TY8Js3h9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fFdaaBB1TY8Js3h9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-fFdaaBB1TY8Js3h9 .icon-shape,#mermaid-svg-fFdaaBB1TY8Js3h9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fFdaaBB1TY8Js3h9 .icon-shape p,#mermaid-svg-fFdaaBB1TY8Js3h9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fFdaaBB1TY8Js3h9 .icon-shape .label rect,#mermaid-svg-fFdaaBB1TY8Js3h9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fFdaaBB1TY8Js3h9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fFdaaBB1TY8Js3h9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fFdaaBB1TY8Js3h9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
线程已经持有 mutex
条件是否成立
pthread_cond_wait
原子地释放 mutex 并进入等待
其他线程取得 mutex
修改共享状态
signal 或 broadcast
等待线程被唤醒
重新竞争并取得 mutex
操作共享数据
释放 mutex
这里的"原子地释放并等待"非常重要。如果释放锁和进入等待分成两个互不关联的步骤,通知可能恰好发生在两步之间,导致线程错过通知并永久等待。
5. 为什么必须用 while,不能只用 if
正确模板:
c
pthread_mutex_lock(&mutex);
while (!condition_is_true()) {
pthread_cond_wait(&cond, &mutex);
}
use_shared_state();
pthread_mutex_unlock(&mutex);
等待返回只表示"线程获得了重新检查条件的机会",不保证条件此刻仍然成立,原因包括:
- 可能发生伪唤醒;
broadcast会同时唤醒多个线程;- 某个线程先获得互斥锁并消耗了资源;
- 状态在当前线程重新取得锁之前再次改变。
因此必须在重新持有互斥锁后再次检查谓词。不成立就继续等待。
错误写法:
c
if (count == 0) {
pthread_cond_wait(&cond, &mutex);
}
/* 醒来后不检查就直接读取,可能出错 */
6. signal 与 broadcast
c
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
| 操作 | 效果 | 常见用途 |
|---|---|---|
pthread_cond_signal |
至少唤醒一个等待者 | 新增一个资源,只需一个线程处理 |
pthread_cond_broadcast |
唤醒所有当前等待者 | 状态变化后可能允许多个线程继续 |
被唤醒不等于马上执行。等待线程从 pthread_cond_wait() 返回前,还要重新获得与等待绑定的互斥锁。
如果没有线程等待,signal 和 broadcast 都不会产生可供以后使用的"通知余额"。
7. 条件变量的初始化与销毁
7.1 静态初始化
c
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
默认属性下,这种写法最简单。
7.2 动态初始化
c
pthread_cond_t cond;
int error = pthread_cond_init(&cond, NULL);
第二个参数为 NULL 表示默认属性。
7.3 销毁
c
int error = pthread_cond_destroy(&cond);
销毁时必须确认没有线程仍在该条件变量上等待,也不会再有线程访问它。
8. 本节核心 API
c
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
8.1 定时等待
pthread_cond_timedwait() 增加一个绝对截止时间。到期时返回 ETIMEDOUT,返回前同样会重新取得互斥锁。
c
#include <errno.h>
#include <time.h>
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += 2;
while (count == 0) {
int error = pthread_cond_timedwait(&cond, &mutex, &deadline);
if (error == ETIMEDOUT) {
break;
}
}
注意:第三个参数通常是绝对时间,不是"等待多少秒"的相对时长。
9. restrict 关键字怎样理解
教材第 164 页从 restrict 引出条件变量接口。它是 C99 的指针限定符,向编译器承诺:在相应作用域内,对相关对象的访问遵守特定的无别名规则,以便编译器优化。
初学阶段需要掌握:
- 它主要是程序员对编译器作出的约定;
- 违反约定可能导致未定义行为;
- 它不是运行时检查;
- 看到函数原型中的
restrict不需要自行删除或改写。
把它简单说成"内存中只能存在一个指针指向该对象"并不严谨。重点是特定作用域内通过哪些指针访问对象,而不是物理上绝不允许其他指针值存在。
10. 录音中的生产者消费者模型
录音从 06:46 开始建立一个长度为 5 的缓冲区:
c
#define BUFFER_SIZE 5
static int buffer[BUFFER_SIZE];
static int count = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
生产者逻辑:
text
加锁
→ 缓冲区满时等待
→ 写入一个数据并增加 count
→ 通知消费者
→ 解锁
消费者逻辑:
text
加锁
→ 缓冲区空时等待
→ 读取一个数据并减少 count
→ 通知生产者
→ 解锁
录音到 15:02 已写完这套基本逻辑,但还没有进入运行结果分析。
11. 简化练习:单槽生产者消费者
为了先练懂等待流程,下面把长度为 5 的数组简化成一个槽位,并让程序生产、消费 5 次后正常退出。
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ITEM_COUNT 5
static int slot;
static int has_item = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t can_produce = PTHREAD_COND_INITIALIZER;
static pthread_cond_t can_consume = PTHREAD_COND_INITIALIZER;
static void check_pthread(int error, const char *operation)
{
if (error != 0) {
fprintf(stderr, "%s: %s\n", operation, strerror(error));
exit(EXIT_FAILURE);
}
}
static void *producer(void *arg)
{
(void)arg;
for (int value = 1; value <= ITEM_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");
}
slot = value;
has_item = 1;
printf("生产:%d\n", value);
check_pthread(pthread_cond_signal(&can_consume),
"pthread_cond_signal");
check_pthread(pthread_mutex_unlock(&mutex),
"pthread_mutex_unlock");
}
return NULL;
}
static void *consumer(void *arg)
{
(void)arg;
for (int i = 0; i < ITEM_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");
}
int value = slot;
has_item = 0;
printf("消费:%d\n", value);
check_pthread(pthread_cond_signal(&can_produce),
"pthread_cond_signal");
check_pthread(pthread_mutex_unlock(&mutex),
"pthread_mutex_unlock");
}
return NULL;
}
int main(void)
{
pthread_t producer_id;
pthread_t consumer_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");
return EXIT_SUCCESS;
}
编译运行:
bash
gcc -Wall -Wextra -pthread cond_single_slot.c -o cond_single_slot
./cond_single_slot
预期得到交替输出:
text
生产:1
消费:1
生产:2
消费:2
...
11.1 为什么示例使用两个条件变量
录音的一生产者、一消费者示例使用一把互斥锁和一个条件变量,也能表达基本过程。
练习代码拆成两个条件变量:
text
can_produce:有空位,可以生产
can_consume:有数据,可以消费
这样通知目标更清楚,也更容易扩展为多个生产者和消费者。互斥锁仍然只有一把,因为两个条件都由同一组共享状态 slot + has_item 决定。
12. 通知与解锁的顺序
常见且易理解的写法是:
c
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? - 条件变量会不会保存一次无人接收的
signal? signal和broadcast有什么区别?- 生产者发现缓冲区满时怎样等待?
- 消费者发现缓冲区空时怎样等待?
- 为什么修改
count或has_item时仍然必须加锁?
最低实践要求:
- 能默写"加锁 → while 检查 → wait → 操作状态 → signal → 解锁";
- 能解释
wait为什么不会造成其他线程永远拿不到互斥锁; - 能运行并修改第 11 节的单槽生产者消费者程序;
- 能把
while改成if后指出潜在错误,而不是只观察一次运行结果。
16. PDF 页码与录音时间索引
| 主题 | PDF 页码 | 录音时间 |
|---|---|---|
| 自旋锁收尾 | 164 | 00:02---01:15 |
| 条件变量用途 | 164 起 | 01:17---01:55 |
restrict 关键字 |
164---165 | 01:56---03:21 |
wait、定时等待、通知与广播 |
165---166 | 03:22---05:52 |
| 条件变量类型与初始化 | 166---168 | 05: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两个条件变量。