除了锁之外,并发程序中另一个最重要的模式就是条件变量,与锁不同,条件变量用于解决线程间的同步问题。
条件变量的基本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要在锁里操作?
这是因为判断count==0或者==1,本身也是一段临界代码,判断后再进入wait是存在并发风险的,所以要在锁中操作,否则可能出现在判断了==0后,马上被修改为1了,但是依旧进入了wait并且再也无法被唤醒。
为什么用while循环判断
在并发场景下,使用while循环判断总是好过if判断。这里涉及到条件变量中的2种语义:
Mesa Semantics:被唤醒的线程需要重新获取锁,并且重新检查条件。实现简单,效率高,是大多数系统的默认语义。
Hoare Semantics:被唤醒的线程立即获得锁,条件保证满足,实现复杂,效率低。
在实际编程中,特别是在使用Pthreads时,你通常会遇到Mesa语义,需要在被唤醒后重新检查条件。
所以,使用while循环其实是一种Mesa语义,因为在被唤醒后继续往下执行时,以消费者为例,线程在count==0时进入wait,在苏醒后,线程无法保证count==1的条件在苏醒后依然成立,即wait被signal后,无法确保count!=0,因为此时也可能存在其他线程修改了count,所以需要再次检查count的值,如果此时依旧不满足,那么继续进入wait。
实现Hoare语义就可以保证,但是就像上面说的,Hoare语义实现非常复杂,而且效率低下。
条件变量的广播
有一种广播式的条件变量,叫做covering condition,就是一次性唤醒所有wait这个条件变量的线程,可以降低复杂度,但是性能开销会比较大。
条件变量怎么实现?
总结
这章主要就介绍了条件变量的使用,并且举了个例子说明如何使用条件变量实现并发程序。