C++ / a working model

135 / 163   ·   C11   ·   约 8 分钟

信号量

先记住这句话

信号量用一个整数计数器加上阻塞与唤醒原语来协调线程。初始化值决定它充当互斥锁还是事件通知器。本章用独立示例说明 wait/post 语义、二元用法以及父子线程排序,并给出可编译的 C 演示。

本篇内容
  1. 计数器模型与两种操作
  2. 把信号量当锁使用
  3. 用信号量强制执行顺序
  4. 运行示例
  5. 动手练习

官方章节 PDF

计数器模型与两种操作

信号量保存一个整数。wait 把该整数减一,若结果变成负数则调用者进入睡眠队列。post 把整数加一,若队列非空则唤醒其中一个等待者。负值的绝对值恰好等于当前睡眠线程数。所有修改必须在原子临界区内完成,因此真正实现通常还需要一把底层锁。

把信号量当锁使用

把计数器设为 1 即可获得互斥。第一个 wait 成功并把计数器变成 0,后续 wait 会阻塞。对应的 post 把计数器加回 1 并可能唤醒一个竞争者。因为只有“持有/未持有”两种可见状态,这种用法常被叫做二元信号量。临界区越短越好,否则会降低并发度。

用信号量强制执行顺序

把计数器设为 0 可以让一个线程等待另一个线程的完成信号。子线程做完工作后执行 post,父线程在 wait 上阻塞直到该信号到达。此模式避免了忙等待,也比 condvar 更轻量,因为不需要额外的谓词检查。多个等待者时唤醒顺序由实现决定,通常是 FIFO。

常见误区

  • 未调用 sem_init 就使用信号量属于未定义行为。
  • 把锁信号量初始化成 0 会使第一个线程永远阻塞。

运行一个例子

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

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

#define N 1000

sem_t lock;
int shared = 0;

void *inc(void *arg) { (void)arg;
    for (int i = 0; i < N; i++) {
        sem_wait(&lock);
        shared++;
        sem_post(&lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t[2];
    sem_init(&lock, 0, 1);
    for (int i = 0; i < 2; i++) {
        pthread_create(&t[i], NULL, inc, NULL);
    }
    for (int i = 0; i < 2; i++) {
        pthread_join(t[i], NULL);
    }
    printf("%d\n", shared);
    sem_destroy(&lock);
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-31-semaphores.c -o example && ./example

预期结果

2000

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

要用信号量保护一段临界区,初始值必须是多少?若误设为 0 会出现什么现象?

查看参考答案

必须是 1。设为 0 时第一个 wait 就会把计数器变成 -1 并睡眠,临界区永远无法进入。

继续查证

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

回到目录