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 先结束上一部分的自旋锁介绍。

互斥锁获取失败时,线程通常进入阻塞状态;自旋锁获取失败时,执行单元持续循环检查锁是否释放。

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(&not_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. 录音与教材表述校正

  1. 转写中的"自选锁"应为"自旋锁(spinlock)"。
  2. 用户空间并非绝对不能使用自旋锁;Linux 提供 pthread_spin_*,只是普通应用通常不适合使用。
  3. 条件变量不是"暂时把条件释放",而是等待线程原子地释放互斥锁并阻塞。
  4. pthread_cond_wait() 返回之前已经重新取得互斥锁。
  5. 被通知只代表条件可能成立,必须使用 while 再次检查共享谓词。
  6. pthread_cond_signal() 唤醒至少一个等待线程;具体哪个线程由调度决定,不能理解为固定或真正的随机选择。
  7. pthread_cond_timedwait() 使用绝对截止时间,并在超时返回前重新取得互斥锁。
  8. restrict 不是"世界上只能存在一个指针",而是关于特定作用域中访问路径的编译优化约定。
  9. 条件变量没有存储通知的计数。没有等待者时发送通知,不会留给未来线程。
  10. 条件变量可以降低无效竞争和轮询,但不能笼统称为"完全无竞争";被唤醒的线程仍要竞争互斥锁。

15. 本节最低掌握标准

学完后应能回答:

  1. 条件变量与互斥锁分别负责什么?
  2. pthread_cond_wait() 调用前和返回后,互斥锁分别是什么状态?
  3. 为什么条件判断必须写成 while?
  4. 条件变量会不会保存一次无人接收的 signal?
  5. signal 和 broadcast 有什么区别?
  6. 生产者发现缓冲区满时怎样等待?
  7. 消费者发现缓冲区空时怎样等待?
  8. 为什么修改 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 以后,本次不整理

进一步核对资料:

17. 下一步学习

本次截止在代码写完、准备运行的位置。下一段应继续观察:

  • 一个条件变量时,生产者和消费者的实际交错顺序;
  • 为什么改变互斥锁的持有范围会改变"一次生产几个、一次消费几个";
  • 怎样正确安排通知位置,避免双方都进入等待;
  • 多生产者、多消费者时为什么通常拆成 not_full 与 not_empty 两个条件变量。
相关推荐
步行cgn1 小时前
Spring AOP 切面的先后顺序详解
java·spring
LRL_1 小时前
【K8s 存储】Longhorn v1.12.1 离线安装全流程:外网制作离线包 + 内网部署(附踩坑实录)
linux·windows·node.js
星恒随风1 小时前
Linux开发工具详解(一):软件包管理、Vim、GCC/G++、Makefile与进度条实战
linux·运维·笔记·学习·vim
小小、码农1 小时前
Ubuntu / Linux 软件安装完整笔记
linux·笔记·ubuntu
ym hyd 1111 小时前
宝宝早教服务系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·spring boot
yunwei371 小时前
eBPF 开发实践:使用 sockops 加速网络请求转发
linux·后端·性能优化
rest10241 小时前
做io测试
linux·服务器·网络
一条破秋裤1 小时前
Linux 网络编程基础:网络分类、分层模型与数据封装
linux·运维·网络
自强的小白2 小时前
微服务之间的依赖传递和调用(实体类的模块传递以及负载均衡的调用!)注册中心
java·微服务