前言
我们先解释一下什么叫惊群, 所谓惊群就是当一个条件或事件触发时,所有等待这个条件或事件的线程或进程都被唤醒了。打个比方,你要去猪圈喂猪,每次你就扔一个馒头,一群猪都会被唤醒,然后去抢馒头,但一次只有一个馒头,只够一头猪吃一口,别的猪,被唤醒了也没用。这就叫惊群,惊动了一群猪。希望能帮助大家理解,呵呵。
pthread_cond_signal惊群
在前面的文章《深入理解Linux 条件变量1:使用场景、接口说明
》中我们介绍pthread_cond_signal和pthead_cond_broadcast时提到:
- pthread_cond_signal :通知条件变量状态变化,能够唤醒至少1个等待该条件的线程。
- pthread_cond_broadcast: 广播通知条件变量状态变化,唤醒所有等待该条件的线程。
注意上面的说法,pthread_cond_signal是 唤醒至少 1个,并不是有且只有1个,这可能是因为内部实现过于复杂,所以该接口现状就如此。那么到底会不会出现一次唤醒了2个线程呢?我们还是通过一个示例代码来验证一下,demo代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t count_lock;
pthread_cond_t count_ready;
int count;
void *thread_consumer(void *arg)
{
char *name = (char *)arg;
for(;;){
pthread_mutex_lock(&count_lock);
printf("[%s]decrement:waiting\n", name);
/*等待满足条件,期间互斥量仍然可用*/
if(count == 0){
pthread_cond_wait(&count_ready, &count_lock);
}
printf("[%s]decrement:count = %d\n", name, count);
if (count == 0)
{
printf("[%s]exit count:%d\n", name, count);
exit(1);
}
count = 0;
pthread_mutex_unlock(&count_lock);
}
pthread_exit(NULL);
}
void *thread_producer(void *arg)
{
sleep(1);
while(1){
pthread_mutex_lock(&count_lock);
count = 1;
pthread_mutex_unlock(&count_lock);
pthread_cond_signal(&count_ready);
}
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid1,tid2,tid3;
const char *tid_name1 = "th_consumer1";
const char *tid_name2 = "th_producer";
const char *tid_name3 = "th_consumer2";
count=0;
pthread_mutex_init(&count_lock, NULL);
pthread_cond_init(&count_ready, NULL);
pthread_create(&tid2, NULL, thread_producer, (void *)tid_name2);
pthread_create(&tid1, NULL, thread_consumer, (void *)tid_name1);
pthread_create(&tid3, NULL, thread_consumer, (void *)tid_name3);
/*等待decrement退出*/
pthread_join(tid2, NULL);
printf("thread_consumer quit\n");
pthread_join(tid3, NULL);
pthread_join(tid1, NULL);
return 0;
}
在上面的示例代码中,在thread_consumer消费者线程里,如果出现了惊群现象,也就是同时唤醒了2个消费线程,则程序会退出,如果不出现惊群现象,程序会正常节奏运行。运行结果如下:
ubuntu@VM-0-17-ubuntu:/opt/test/cond-test$ ./test
[th_consumer1]decrement:waiting
[th_consumer2]decrement:waiting
[th_consumer2]decrement:count = 1
[th_consumer2]decrement:waiting
[th_consumer2]decrement:count = 1
[th_consumer2]decrement:waiting
[th_consumer1]decrement:count = 1
[th_consumer1]decrement:waiting
[th_consumer2]decrement:count = 0
[th_consumer2]exit count:0
果然,很快就出现了惊群现象。
条件变量惊群现象解决方法
我们还是拿前面喂猪的例子来分析,如果这群猪都比较聪明,每次被唤醒后,先检查一下嘴巴跟前有没有多余的馒头,如果有就起来吃,没有就继续睡觉等待,是不是就避免了猪乱抢的结果。我们按照这个思路来修改一下上面消费线程的实现,如下所示:
void *thread_consumer(void *arg)
{
char *name = (char *)arg;
for(;;){
pthread_mutex_lock(&count_lock);
printf("[%s]decrement:waiting\n", name);
/*等待满足条件,期间互斥量仍然可用*/
while(count == 0){
pthread_cond_wait(&count_ready, &count_lock);
}
printf("[%s]decrement:count = %d\n", name, count);
if (count == 0)
{
printf("[%s]exit count:%d\n", name, count);
exit(1);
}
count = 0;
pthread_mutex_unlock(&count_lock);
}
pthread_exit(NULL);
}
我们使用while循环来检查条件,当线程醒来后,因为还在while循环内,所以还会在判断count是否为0,如果为0,就返回继续等待,这样就能解决pthread_cond_signal的惊群现象了。
小结
pthread_cond_signal不能保证每次只唤醒1个等待线程,可能会唤醒多个,所以会导致惊群现象,为了解决惊群现象,我们可以在pthread_cond_wait前面 通过while 判断 条件变量标识的队列或变量的状态是否可用,只有可用才去消费,如果不可用,则继续返回等待。