C++ / a working model

134 / 163   ·   C11   ·   约 8 分钟

条件变量

先记住这句话

条件变量让线程能够高效地休眠直到某个共享条件变为真,从而避免无意义的自旋。它必须与互斥锁配合,通过wait原子释放锁并睡眠、signal唤醒等待者,并依靠显式状态变量防止信号丢失。

本篇内容
  1. 锁无法解决的等待问题
  2. wait与signal的语义
  3. 安全使用的三条铁律
  4. 运行示例
  5. 动手练习

官方章节 PDF

锁无法解决的等待问题

互斥锁只能保证临界区互斥,却无法让线程在条件尚未成立时真正休息。若线程反复检查一个标志,处理器会被白白占用。我们需要一种原语,使线程进入睡眠队列,直到别的线程改变状态后再被唤醒。

wait与signal的语义

调用wait时必须已经持有配套的互斥锁。该调用原子地释放锁并把调用者放入条件变量的等待队列;被唤醒后它会重新获得同一把锁才返回。signal则从队列中选出一个线程并使其可运行。两者都必须围绕一个记录真实条件的状态变量来使用。

安全使用的三条铁律

第一,永远在持有锁的情况下执行wait和signal。第二,用while而不是if重新测试条件,以抵御虚假唤醒。第三,必须维护独立的状态变量,否则过早发出的signal会被丢弃,导致等待者永久睡眠。

常见误区

  • 未持锁就调用wait会立刻产生未定义行为
  • 用if代替while检查条件会因虚假唤醒而出错
  • 缺少状态变量时,子线程先结束发出的signal会被忽略

运行一个例子

最低标准 C11 · 完整程序 · 下载 .c

#include <stdio.h>
#include <pthread.h>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int finished = 0;

static void *child_thread(void *arg) { (void)arg;
    (void)arg;
    printf("child running\n");
    pthread_mutex_lock(&mtx);
    finished = 1;
    pthread_cond_signal(&cv);
    pthread_mutex_unlock(&mtx);
    return NULL;
}

int main(void) {
    printf("parent starting\n");
    pthread_t tid;
    pthread_create(&tid, NULL, child_thread, NULL);
    pthread_mutex_lock(&mtx);
    while (finished == 0) {
        pthread_cond_wait(&cv, &mtx);
    }
    pthread_mutex_unlock(&mtx);
    printf("parent finishing\n");
    pthread_join(tid, NULL);
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-30-condition-variables.c -o example && ./example

预期结果

parent starting
child running
parent finishing

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

父线程在子线程已经signal之后才调用wait,且代码里没有done之类的状态变量,程序会怎样?

查看参考答案

signal被丢弃,父线程随后进入wait后永远不会被唤醒,程序挂起。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录