pthread 条件变量解析:原子等待、虚假唤醒与 Completion 机制

作者:CherryYang 发布时间: 2026-08-12 阅读量:9 评论数:0

pthread 条件变量解析:原子等待、虚假唤醒与 Completion 机制

核心结论:条件变量不保存业务条件,它负责让线程高效睡眠和被唤醒;真正的条件必须保存在 mutex 保护的共享状态中。

问题背景

假设主线程必须等待 worker 完成某项工作,最直观的写法是不断检查共享变量:

while (!done) {
    // 继续检查
}

这种写法存在两个问题:

  1. 普通变量被多个线程并发读写会产生 data race,属于未定义行为。
  2. 即使把 done 改成原子变量,循环仍会持续占用 CPU,这种方式称为 busy waiting(忙等)。

也可以在循环中调用 sleep(),但这只能降低 CPU 消耗,无法同时兼顾响应延迟。睡眠时间太长会延迟处理,太短又会退化成频繁轮询。

条件变量 pthread_cond_t 的作用,是在条件不满足时把线程挂起,并在共享状态可能发生变化时唤醒它。

“条件”不在 pthread_cond_t 里面

下面是一个简化的 Completion 对象:

#include <pthread.h>
#include <stdint.h>

typedef struct {
    uint32_t done;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} completion_t;

三个成员的职责完全不同:

成员 职责
done 保存真正的业务状态,表示有多少次完成事件尚未被消费
mutex 保护 done,保证检查状态和修改状态不会并发冲突
cond 管理等待线程,并在状态可能改变时通知它们重新检查

因此,“等待条件”是 done > 0,而不是 condpthread_cond_t 本身通常不记录 done 是否为真,也不会理解业务状态的含义。

条件变量这个名称更准确的理解是:用于等待某个条件的变量,而不是“内部装着条件的变量”。

一个完整的 Completion 实现

#include <limits.h>
#include <pthread.h>
#include <stdint.h>

typedef struct {
    uint32_t done;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} completion_t;

int completion_init(completion_t *completion)
{
    int result;

    completion->done = 0;

    result = pthread_mutex_init(&completion->mutex, NULL);
    if (result != 0) {
        return result;
    }

    result = pthread_cond_init(&completion->cond, NULL);
    if (result != 0) {
        pthread_mutex_destroy(&completion->mutex);
        return result;
    }

    return 0;
}

void wait_for_completion(completion_t *completion)
{
    pthread_mutex_lock(&completion->mutex);

    while (completion->done == 0) {
        pthread_cond_wait(&completion->cond, &completion->mutex);
    }

    completion->done--;
    pthread_mutex_unlock(&completion->mutex);
}

void complete(completion_t *completion)
{
    pthread_mutex_lock(&completion->mutex);
    completion->done++;
    pthread_cond_signal(&completion->cond);
    pthread_mutex_unlock(&completion->mutex);
}

void complete_all(completion_t *completion)
{
    pthread_mutex_lock(&completion->mutex);
    completion->done = UINT32_MAX;
    pthread_cond_broadcast(&completion->cond);
    pthread_mutex_unlock(&completion->mutex);
}

void completion_destroy(completion_t *completion)
{
    pthread_cond_destroy(&completion->cond);
    pthread_mutex_destroy(&completion->mutex);
}

这是用于理解机制的简化实现:

  • complete() 产生一次完成事件,并唤醒一个等待者。
  • wait_for_completion() 消费一次完成事件,因此会执行 done--
  • complete_all() 把所有当前等待者唤醒,并把 done 设置为一个极大值,使它们都能通过检查。

实际工程实现还需要处理计数溢出、错误码、超时等待和销毁时机等问题,不能直接把这段教学代码当成完整基础库。

pthread_cond_wait() 到底做了什么

调用 pthread_cond_wait() 之前,线程必须已经持有对应的 mutex:

pthread_mutex_lock(&completion->mutex);

while (completion->done == 0) {
    pthread_cond_wait(&completion->cond, &completion->mutex);
}

pthread_cond_wait() 内部完成以下过程:

  1. 把当前线程加入条件变量的等待集合。
  2. 释放传入的 mutex。
  3. 挂起当前线程,使其不再占用 CPU 轮询。
  4. 收到 signalbroadcast 后被唤醒。
  5. 重新竞争并获得 mutex。
  6. 持有 mutex 后,从 pthread_cond_wait() 返回。

最关键的是前两步:加入等待集合和释放 mutex 在逻辑上是一个原子等待操作

这里的“原子”不是说整个睡眠过程不可打断,而是说通知线程不可能恰好插入“等待线程已经释放锁、但还没有真正进入等待集合”的缝隙中,从而造成永久错过通知。

为什么不能手动解锁后再等待

假设存在一个看似合理的接口:

pthread_mutex_unlock(&mutex);
ordinary_wait();

执行过程可能变成:

  1. 等待线程检查 done == 0
  2. 等待线程释放 mutex。
  3. worker 获得 mutex,把 done 改为 1,发送通知后离开。
  4. 等待线程此时才调用 ordinary_wait()
  5. 通知已经发生,等待线程却开始睡眠,并且可能永远无法醒来。

这就是 lost wakeup(丢失唤醒)问题。

pthread_cond_wait() 将“释放锁”和“进入等待状态”绑定在一个协议中。通知线程也必须使用同一个 mutex 修改条件,才能让检查状态、进入等待和发送通知形成正确的同步关系。

条件变量不是一种特殊锁。mutex 负责互斥访问共享状态,条件变量负责在 mutex 之外睡眠并等待状态变化,两者解决的问题不同。

为什么已经被唤醒还要使用 while

下面的写法不正确:

if (completion->done == 0) {
    pthread_cond_wait(&completion->cond, &completion->mutex);
}

completion->done--;

被唤醒只表示“状态可能发生了变化”,不保证当前线程一定可以继续执行。必须使用 while 重新检查条件,主要有三个原因。

1. 虚假唤醒

POSIX 允许 pthread_cond_wait() 在没有对应业务事件时返回。程序必须允许这种 spurious wakeup,并重新检查谓词。

2. 多个等待者之间存在竞争

多个线程可能同时被唤醒,但在它们依次重新获得 mutex 时,完成事件可能已经被第一个线程消费。后续线程必须重新进入等待。

3. 通知不等于资源所有权

pthread_cond_signal() 表示“共享状态可能满足条件”,它不把 done 对应的事件直接授予某个指定线程。真正决定能否继续的是受 mutex 保护的谓词。

因此,条件变量的标准模板始终是:

pthread_mutex_lock(&mutex);

while (!predicate) {
    pthread_cond_wait(&cond, &mutex);
}

// 在持有 mutex 时使用或消费 predicate 对应的状态

pthread_mutex_unlock(&mutex);

signal 和 broadcast 的区别

pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
  • pthread_cond_signal() 至少唤醒一个等待线程,适合一次状态变化只允许一个线程继续的场景。
  • pthread_cond_broadcast() 唤醒全部等待线程,适合配置切换、系统关闭或“所有线程都可以继续”的状态变化。

broadcast 不代表所有线程最终都能通过条件检查。所有线程醒来后仍要逐个竞争 mutex,并在 while 中重新检查谓词。

通知通常放在修改谓词所使用的 mutex 临界区内,这样状态变化和通知保持清晰的一致关系:

pthread_mutex_lock(&mutex);
predicate = true;
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);

Completion 和 pthread_join() 的区别

pthread_join() 等待的是线程生命周期结束:

pthread_create(&worker, NULL, worker_entry, argument);
pthread_join(worker, NULL);

它有两个主要作用:

  1. 阻塞调用线程,直到目标线程从入口函数返回或调用 pthread_exit()
  2. 回收 joinable 线程的相关资源,并可取得线程返回值。

Completion 等待的则是线程执行过程中的某个事件。线程发出 complete() 后可以继续运行,并不需要退出。

对比项 Completion pthread_join()
等待对象 某个阶段或事件完成 整个线程退出
完成后 worker 能否继续运行 可以 不可以,线程已经结束
能否在一个线程中通知多次 可以 不可以,一个线程只能被成功 join 一次
是否负责回收线程资源

如果 worker 只计算一次结果,随后立即退出,那么主线程直接 pthread_join() 就能保证结果已经写完,此时额外使用 wait_for_completion() 通常没有必要。

pthread_join() 紧跟在 pthread_create() 后面虽然合法,但会让主线程立刻阻塞,整体表现接近串行。通常应先启动所需线程或完成主线程自己的工作,再在确实需要等待退出的位置调用 join

Completion 更符合本意的场景

worker 启动后需要初始化设备、加载索引或建立网络连接。主线程必须等待初始化完成才能接受请求,但 worker 初始化后还要长期运行:

下面省略初始化、服务循环和停止控制的业务实现,只展示 Completion 与 pthread_join() 各自所处的同步位置:

typedef struct {
    completion_t ready;
    /* 其他运行状态 */
} service_t;

void *service_worker(void *argument)
{
    service_t *service = argument;

    initialize_storage_and_network();
    complete(&service->ready);

    serve_requests_until_stopped();
    cleanup_service();
    return NULL;
}

int main(void)
{
    service_t service;
    pthread_t worker;

    completion_init(&service.ready);
    pthread_create(&worker, NULL, service_worker, &service);

    wait_for_completion(&service.ready);
    start_accepting_client_requests();

    request_service_stop();
    pthread_join(worker, NULL);
    completion_destroy(&service.ready);
    return 0;
}

这里的两个等待点语义不同:

  • wait_for_completion() 等待“服务已经准备好”,返回时 worker 仍在运行。
  • pthread_join() 等待“服务已经彻底停止”,返回时 worker 已经退出。

这正是 Completion 存在的意义:把线程内部的阶段性事件,与线程整体生命周期分开同步。

总结

pthread 条件变量的正确模型是“mutex 保护谓词,cond 负责等待通知,while 负责重新验证”;Completion 在其上封装可消费的完成状态,而 pthread_join() 负责的是线程退出和资源回收。

评论