条件变量是一个用于存放等待线程的队列,使线程在条件不满足时进入队列睡眠,并由其他线程在条件改变后从队列中唤醒它们的同步机制。

API

  • pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*)
  • pthread_cond_signal(pthread_cond_t*)
  • pthread_cond_broadcast()
  • pthread_cond_destroy(pthread_cond_t*)

使用常规

使用条件变量一定要搭配一个状态变量 (state variable) 与一个互斥锁

  • 状态变量:条件变量本身是一个「等待队列」,其不存储关心的条件是否满足的信息;因此需要一个显式的状态变量(通常是布尔值,计数器等)来记录。
  • 互斥锁:条件变量的睡眠与唤醒操作必须是原子的,且多进程共享的状态变量也需要保护。

线程被 pthread_cond_wait() 阻塞时释放互斥锁。

生产者/消费者问题

又叫有界缓冲区 (bounded buffer) 问题,由 Dijkstra 提出(这里也能见到你!)。

生产与消费均为 1 单位。

Producer
1
2
3
4
5
6
7
8
9
10
void *producer(void *arg) {
for (int i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex);
while (count == MAX)
Pthread_cond_wait(&empty, &mutex);
put(i);
Pthread_cond_signal(&fill);
Pthread_mutex_unlock(&mutex);
}
}
Consumer
1
2
3
4
5
6
7
8
9
10
11
void *consumer(void *arg) {
for (int i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex);
while (count == 0)
Pthread_cond_wait(&fill, &mutex);
int tmp = get();
Pthread_cond_signal(&empty);
Pthread_mutex_unlock(&mutex);
printf("%d\n", tmp);
}
}
  • 状态变量 count
  • 双条件变量
    • empty:确定当前缓冲区未满,从队列中唤醒一名生产者进行生产
    • fill:确定当前缓冲区非空,从队列唤醒一名消费者进行消费
  • 使用 while 而非 if:避免被唤醒与修改缓冲区间的中断产生影响

广播

生产者/消费者问题也可以(丑陋地)使用单条件变量解决。

如果仅仅把代码中的双条件变量改为单个是不够的。考虑一个单生产者双消费者的例子:

  • 生产者生产 1 单位,唤醒消费者 A,睡眠
  • 消费者 A 消费 1 单位,唤醒消费者 B,睡眠
  • 缓冲区为空,消费者 B 未通过 while 条件检查,睡眠
  • 此时所有线程都陷入睡眠,死锁

可以发现单条件变量的问题在于无法唤醒需要被唤醒的线程。

Lampson 与 Redell 的方案 条件覆盖 (Covering Conditions):把所有线程都唤醒不就好了?那些不需要被唤醒的线程将因为无法通过 while 条件检查继续睡眠。

  • 问题解决:把所有 Pthread_cond_signal() 换成 Pthread_cond_broadcast() 即可。