1.条件变量:
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量之所以要和互斥锁一起使用,其实条件变量本质上是一个等待队列,会把所有阻塞等待的线程放置在等待队列,而互斥量用来保护等待队列,所以条件变量通常和互斥锁一起使用,这样做其实可以防止唤醒丢失。
条件变量使线程可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制。
由两个部分组成:
- wait端:一个线程等待"条件变量的条件成立"而挂起;
- signal/broadcast端:另一个线程使"条件成立"(发出条件成立信号)。
2.常用函数:
条件对象的数据类型是 pthread_cond_t ;
条件对象的初始方式分为静态方式和动态方式:
//静态初始化方式:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态初始化方式:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//销毁一个条件变量,只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);
//阻塞等待一个条件变量是否满足条件
//参数cond指定条件变量,参数mutex指定互斥量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//限时阻塞等待一个条件变量,函数成功返回0,超时返回的错误码是ETIMEDOUT
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
//唤醒至少一个阻塞在条件变量上的线程,也有可能会唤醒多个线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒全部阻塞在条件变量上的线程,由于函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
3.等待线程:wait端
1.使用pthread_cond_wait前要先加锁
2.pthread_cond_wait内部会先将线程放入等待队列,然后解锁,最后等待条件变量被其它线程激活
3.pthread_cond_wait被激活后内部会再自动加锁
pthread_mutex_lock();
while (condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
4.激活线程:signal或broadcast端
1.加锁(和等待线程用同一个锁)
2.pthread_cond_signal发送信号
3.解锁
pthread_mutex_lock();
pthread_cond_signal/broadcast();
pthread_mutex_unlock();
5.在等待线程wait端,我们必须把判断布尔条件和wait()放到while循环中,而不能用if语句,原因是虚假唤醒(spurious wakeup)
那么,究竟什么是虚假唤醒,导致虚假唤醒的原因又是什么呢?
一般来说,在多线程竞争一个资源的时候,会用到pthread_cond_wait,pthread_cond_signal机制,典型的做法就是在一个使用这个资源的线程(消费者)里面,判断资源如果不可用的话,则pthread_cond_wait,在另外一个线程(生产者)中判断如果资源可用的话,则发一个pthread_cond_signal或者pthread_cond_broadcast通知wait的线程。
但是有一个问题,就是在wait成功后,实际上此时的资源是否就一定可用呢?答案是否定的。
举个例子,我们现在有一个生产者线程,两个消费者线程,一个队列。
(1) 1号线程从队列中获取了一个元素,此时队列变为空。
(2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。
(3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。
(4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
(5) 然而可能出现这样的情况:当2号线程准备获得队列的锁,去获取队列中的元素时,此时1号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,1号线程便获得队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,然后释放队列锁。
(6) 等到2号线程获得队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,所以对于2号线程而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。
在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果就是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应就称为“虚假唤醒”
也就是说,即使没有线程调用condition_signal, 原先调用condition_wait的函数也可能会返回。此时线程被唤醒了,但是条件并不满足,这个时候如果不对条件进行检查而往下执行,就可能会导致后续的处理出现错误。
虚假唤醒在linux的多处理器系统中/在程序接收到信号时可能回发生。在Windows系统和JAVA虚拟机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造成的错误,因此程序的逻辑就变成了while循环的情况。
注意:即使是虚假唤醒的情况,线程也是在成功锁住mutex后才能从condition_wait()中返回。即使存在多个线程被虚假唤醒,但是也只能是一个线程一个线程的顺序执行,也即:lock(mutex) 检查/处理 condition_wai()或者unlock(mutex)来解锁.
如果用if判断,多个等待线程在满足if条件时都会被唤醒(虚假的),但实际上条件并不满足,生产者生产出来的消费品已经被第一个线程消费了。
这就是我们使用while去做判断而不是使用if的原因:因为等待在条件变量上的线程被唤醒有可能不是因为条件满足而是由于虚假唤醒。所以,我们需要对条件变量的状态进行不断检查直到其满足条件,不仅要在pthread_cond_wait前检查条件是否成立,在pthread_cond_wait之后也要检查。
6.程序例子
8 #include<iostream>
9 #include<unistd.h>
10 #include<pthread.h>
11 #include<stdio.h>
12 using namespace std;
13
14 pthread_mutex_t counter_lock = PTHREAD_MUTEX_INITIALIZER;
15 pthread_cond_t counter_nonzero = PTHREAD_COND_INITIALIZER;
16
17 int counter = 0;
18 int estatus = -1;
19
20 void *decrement_counter(void *argv);
21 void *increment_counter(void *argv);
22
23 int main(int argc, char **argv)
24 {
25 printf("counter: %d\n", counter);
26 pthread_t thd1, thd2;
27 int ret;
28
29 ret = pthread_create(&thd1, NULL, decrement_counter, NULL);
30 if(ret){
31 perror("del:\n");
32 return 1;
33 }
34
35 ret = pthread_create(&thd2, NULL, increment_counter, NULL);
36 if(ret){
37 perror("inc: \n");
38 return 1;
39 }
41 int counter = 0;
42 while(counter != 10){
43 printf("counter(main): %d\n", counter);
44 sleep(1);
45 counter++;
46 }
47
48 return 0;
49 }
50
51 void *decrement_counter(void *argv)
52 {
53 printf("counter(decrement): %d\n", counter);
54 pthread_mutex_lock(&counter_lock);
55 while(counter == 0)
56 pthread_cond_wait(&counter_nonzero, &counter_lock); //进入阻塞(wait),等待激活(signal)
57
58 printf("counter--(before): %d\n", counter);
59 counter--; //等待signal激活后再执行
60 printf("counter--(after): %d\n", counter);
61 pthread_mutex_unlock(&counter_lock);
62
63 return &estatus;
64 }
65
66 void *increment_counter(void *argv)
67 {
68 printf("counter(increment): %d\n", counter);
69 pthread_mutex_lock(&counter_lock);
70 if(counter == 0)
71 pthread_cond_signal(&counter_nonzero); //激活(signal)阻塞(wait)的线程(先执行完signal线程,然后再执行wait线程)
72
73 printf("counter++(before): %d\n", counter);
74 counter++;
75 printf("counter++(after): %d\n", counter);
76 pthread_mutex_unlock(&counter_lock);
77
78 return &estatus;
79 }
结果:
[xqs@localhost ~/test_example]g++ test_cond.cpp -o main -lpthread
[xqs@localhost ~/test_example]./main
counter: 0
counter(main): 0
counter(decrement): 0
counter(increment): 0
counter++(before): 0
counter++(after): 1
counter--(before): 1
counter--(after): 0
counter(main): 1
counter(main): 2
counter(main): 3
counter(main): 4
counter(main): 5
counter(main): 6
counter(main): 7
counter(main): 8
counter(main): 9
7.条件变量的优点
相较于mutex而言,条件变量可以减少竞争。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果临界区中没有数据,消费者之间竞争互斥锁是无意义的。加入条件变量机制以后,只有当生产者完成生产,才会引起消费者之间的竞争,从而提高了程序效率。
8.唤醒丢失问题
在线程未获得相应的互斥锁时调用pthread_cond_signal或pthread_cond_broadcast函数可能会引起唤醒丢失问题。
唤醒丢失往往会在下面的情况下发生:
- 一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
- 另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
- 没有线程正在处在阻塞等待的状态下。
参考:
https://blog.csdn.net/q5707802/article/details/79251018
https://blog.csdn.net/ithomer/article/details/6031723