855 字
4 分钟
Ostep 30 Condition Variables

除了锁之外,并发程序中另一个最重要的模式就是条件变量,与锁不同,条件变量用于解决线程间的同步问题。

条件变量的基本api#

主要由2个方法就可以实现,

pthread_cond_wait(pthread_cond_tc, pthread_mutex_tm);

pthread_cond_signal(pthread_cond_t*c);

一个wait,用于进入阻塞状态等待,一个signal,用于唤醒等待的线程。

为什么需要等待和被唤醒?我们当然可以写一个while循环无限检查一个公共变量来达到效果,但是从锁的实现我们就可以看出,自旋是最消耗性能的做法,它只在一开始自旋有限次数时,通过线程切换成本的降低,来具备较好的性能。

消费者,生产者队列#

这里主要使用一个例子,即如何通过wait和signal2个条件变量的原语,来实现一个经典模型:消费者生产者队列。

这边直接贴最终版的,并且倒着去解释一些关键点了,原文中是从一堆错误慢慢演进的。

cond_t empty, fill;
mutex_t mutex;
int count = 0;
int loops = 10;
void* producer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex);
while (count == 1)
Pthread_cond_wait(&empty, &mutex);
put(i);
Pthread_cond_signal(&fill);
Pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
int i;
for (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);
}
return NULL;
}

有以下几个要点:

为什么wait和signal要在锁里操作?

​ 这是因为判断count0或者1,本身也是一段临界代码,判断后再进入wait是存在并发风险的,所以要在锁中操作,否则可能出现在判断了==0后,马上被修改为1了,但是依旧进入了wait并且再也无法被唤醒。

为什么用while循环判断

在并发场景下,使用while循环判断总是好过if判断。这里涉及到条件变量中的2种语义:

Mesa Semantics:被唤醒的线程需要重新获取锁,并且重新检查条件。实现简单,效率高,是大多数系统的默认语义。

Hoare Semantics:被唤醒的线程立即获得锁,条件保证满足,实现复杂,效率低。

在实际编程中,特别是在使用Pthreads时,你通常会遇到Mesa语义,需要在被唤醒后重新检查条件。

所以,使用while循环其实是一种Mesa语义,因为在被唤醒后继续往下执行时,以消费者为例,线程在count0时进入wait,在苏醒后,线程无法保证count1的条件在苏醒后依然成立,即wait被signal后,无法确保count!=0,因为此时也可能存在其他线程修改了count,所以需要再次检查count的值,如果此时依旧不满足,那么继续进入wait。

实现Hoare语义就可以保证,但是就像上面说的,Hoare语义实现非常复杂,而且效率低下。

条件变量的广播#

有一种广播式的条件变量,叫做covering condition,就是一次性唤醒所有wait这个条件变量的线程,可以降低复杂度,但是性能开销会比较大。

条件变量怎么实现?#

总结#

这章主要就介绍了条件变量的使用,并且举了个例子说明如何使用条件变量实现并发程序。

Ostep 30 Condition Variables
https://www.intotw.cn/posts/ostep-30-condition-variables/
作者
intotw
发布于
2024-07-16
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 805 天

部分内容可能已过时