互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(其他共享资源)的状态变化通知其他线程,并让其他线程等待(堵塞于)这一通知。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
- 一个线程等待"条件变量的条件成立"而挂起;
- 另一个线程使"条件成立"(给出条件成立信号)。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
初始化
用静态分配的条件变量
条件变量的数据类型是 pthread_cond_t 。类似于互斥量,使用条件变量前必须对其初始化。对于经由静态分配的条件变量,将其赋值为 PTHREAD_COND_INITALIZER 即完成初始化操作。可参考下面的例子:
pthread_cond_t cond = PTHREAD_COND_INITALIZER
动态分配的条件变量
使用函数 pthread_cond_init()对条件变量进行动态初始化。需要使用 pthread_cond_init()的情形类似于使用 pthread_mutex_init()来动态初始化互斥量的情况。亦即,对自动或动态分配的条件变量进行初始化时,或是对未采用默认属性经由静态分配的条件变量进行初始化时,必须使用 pthread_cond_init()。
NAME
pthread_cond_destroy, pthread_cond_init - destroy and initialize condition variables
SYNOPSIS
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
SUSv3 规定,对业已初始化的条件变量进行再次初始化,将导致未定义的行为。应当避免这一做法
当不再需要一个经由自动或动态分配的条件变量时,应调用 pthread_cond_destroy()函数予以销毁。对于使用 PTHREAD_COND_INITIALIZER 进行静态初始化的条件变量,无需调用pthread_cond_destroy()。
经 pthread_cond_destroy()销毁的条件变量,之后可以调用 pthread_cond_init()对其进行重新初始化
通知和等待条件变量
条件变量的主要操作是发送信号(signal)和等待(wait):
- 发送新操作即通知一个或者多个出于等待状态的信号,某个共享变量的状态已经改变
- 等待操作是指在收到一个通知前一直处于阻塞状态
函数 pthread_cond_signal()和 pthread_cond_broadcast()均可针对由参数 cond 所指定的条件变量而发送信号。pthread_cond_wait()函数将阻塞一线程,直至收到条件变量 cond 的通知。
NAME
pthread_cond_broadcast, pthread_cond_signal - broadcast or signal a condition
pthread_cond_timedwait - wait on a condition
SYNOPSIS
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
函数 pthread_cond_signal()和 pthread_cond_broadcast()之间的差别在于,二者对阻塞于 pthread_ cond_wait()的多个线程处理方式不同。
- pthread_cond_signal()函数只保证唤醒至少一条遭到阻塞的线程
- pthread_cond_broadcast()则会唤醒所有遭阻塞的线程
使用pthread_cond_broadcast()总能产生正确结果(因为所有线程都能处理多余和虚假唤醒),但是pthread_cond_signal()更高效。不过,只有当仅需唤醒一条(而且无论是其中哪条)等待线程来处理共享变量的状态变化是,才使用pthread_cond_signal()。应用这种方式的典型情况是,所有等待线程都在执行完全相同的任务。基于这些假设,函数pthread_cond_signal()会比 pthread_cond_broadcast()更具效率,因为这可以避免发生如下情况。
- 同时唤醒所有等待线程
- 某一线程首先获得调度。此线程检查了共享变量的状态(在互斥量的保护下),发现还有任务需要完成。该线程执行了所需工作,并改变共享状态,以表明任务完成,最后释放对相关互斥量的锁定
- 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,余下的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用 pthread_cond_wait())。
相形之下,函数 pthread_cond_broadcast()所处理的情况是:处于等待状态的所有线程执行的任务不同(即各线程关联于条件变量的判定条件不同)
ps,所谓的虚假唤醒,形象的意思就是你妈同时通知你爸和你去做饭,做饭只需要一个人做即可, 当你去准备做饭时,你爸已经在做饭了,所以你就相当于被虚假唤醒了。
条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。发送信号时如果没有任何线程在等待该条件变量,这个信号也会不了了之。线程如果在此后等待该条件变量,只有当收到此变量的下一信号时,方可接触阻塞状态。
函数 pthread_cond_timedwait()与函数 pthread_cond_wait()几近相同,唯一的区别在于,由参数 abstime 来指定一个线程等待条件变量通知时休眠时间的上限
NAME
pthread_cond_timedwait- wait on a condition
SYNOPSIS
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参数 abstime 是一个 timespec 类型的结构,用以指定自 Epoch以来以秒和纳秒(nanosecond)为单位表示的绝对(absolute)时间。如果 abstime 指定的时间间隔到期且无相关条件变量的通知,则返回 ETIMEOUT 错误
条件变量总是要与一个互斥量相关。将这些对象通过函数参数传递给 pthread_cond_wait(),后者执行如下操作步骤
- 解锁互斥量 mutex。
- 堵塞调用线程,直至另一线程就条件变量 cond 发出信号。
- 重新锁定 mutex。
设计 pthread_cond_wait()执行上述步骤,是因为通常情况下代码会以如下方式访问共享变量
[Butenhof, 1996]指出,在某些实现中,先解锁互斥量再通知条件变量可能比反序执行效率要高.。如果仅在发出条件变量信号后才解锁互斥量,执行pthread_cond_wait()调用的线程可能会比在互斥量量仍处于加锁状态时就醒来,当其发现互斥量仍未解锁,会立即再次休眠。这会导致两个多余的上下文切换(context switch)。有些实现运用等待变形(wait morphing)技术解决了这一问题:将等待接收信号的线程从条件变量的等待队列转移至互斥量等待队列。这样,即便互斥量处于加锁状态,也无需切换上下文
测试条件变量的判断条件
每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量。注意这里一般有一个通用的设计原则:必须由由一个 while循环,而不是 if 语句,来控制对 pthread_cond_wait()的调用。
这是因为,当代码从pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该立即重新检测判断条件,在条件不满足的情况下继续休眠等待。
从pthread_cond_wait()返回时,之所以不能对判断条件的状态做任何假设,理由如下:
- 其他线程可能会先醒来。也行由多个线程在等待获取与条件变量相关的互斥量。即使就互斥量发出通知的线程将判断条件置为预期状态,其他线程依然有可能率先获取互斥量并改变相关共享变量的状态,进而改变判断条件的状
- 可能会发生虚假唤醒的情况。在一些实现中,即使没有任何其他线程真地就条件变量发出信号,等待此条件变量的线程仍有可能醒来。在一些多处理器系统上,为确保高效实现而采用的技术会导致此类(不常见的)虚假唤醒。SUSv3 对此予以明确认可
C++11中的condition_variable
C++11中提供了condition_variable函数,实际上是对上面函数的封装。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥量结合在一起。
#include <condition_variable>
C++11中提供了两种封装:condition_variable和condition_variable_any
- 相同点:两者都能与std::mutex一起使用。
- 不同点:前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。condition_variable_any会产生额外的开销。
一般只推荐使用condition_variable。除非对灵活性有硬性要求,才会考虑condition_variable_any。
条件变量的构造函数:
std::condition_variable::condition_variable
constructor:
condition_variable(); //默认构造函数无参
condition_variable(const condition_variable&) = delete; //删除拷贝构造函数
条件变量的wait函数:
void wait( std::unique_lock<std::mutex>& lock );
//Predicate是lambda表达式。
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
//以上二者都被notify_one())或notify_broadcast()唤醒,但是
//第二种方式是唤醒后也要满足Predicate的条件。
//如果不满足条件,继续解锁互斥量,然后让线程处于阻塞或等待状态。
//第二种等价于
while (!pred())
{
wait(lock);
}
例子
pthread_cond_wait的机制比较难里理解,是条件变量中重要的成分。条件变量用于线程间同步,那么pthread_cond_wait必须和互斥锁同时作用在一个线程里,它同时起到对资源的加锁和解锁,看下面的示例:
程序创建了2个新线程使他们同步运行,实现进程t_b打印9以内3的倍数,t_a打印其他的数,程序开始线程t_b不满足条件等待,线程t_a运行使a循环加1并打印。直到i为3的倍数时,线程t_a发送信号通知进程t_b,这时t_b满足条件,打印i值。
#include <iostream>
#include "hthread.h"
#include "hmutex.h"
#include "htime.h"
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//init cond
int i = 1; //global
void *thread1(void *junk){
for(;i < 9; i++){
pthread_mutex_lock(&mutex);
printf("call thread1, id = %lu \n", pthread_self());
if(i % 3 == 0){
pthread_cond_signal(&cond);
}else{
printf("thread1: %d\n",i);
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
// 0[2]
void *thread2(void*junk){
for (; i < 9;){
pthread_mutex_lock(&mutex);
printf("call thread2 id = %lu \n", pthread_self());
if(i % 3 != 0){ // begin i = 1, 1 %2 != 0
pthread_cond_wait(&cond, &mutex);
}
printf("thread2: %d\n",i);
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(int argc, char* argv[]) {
printf("call main id = %lu \n", pthread_self());
pthread_t t_a;
pthread_t t_b;//two thread
pthread_create(&t_a,NULL,thread2,(void*)NULL); // when creat thread, thread2 run
pthread_create(&t_b,NULL,thread1,(void*)NULL);//Create thread
pthread_join(t_b,NULL);//wait a_b thread end
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
call main id = 139821601752896
call thread1, id = 139821571933952
thread1: 1
call thread2 id = 139821580326656
call thread1, id = 139821571933952
thread1: 2
call thread1, id = 139821571933952
thread2: 3
call thread1, id = 139821571933952
thread1: 4
call thread2 id = 139821580326656
call thread1, id = 139821571933952
thread1: 5
call thread1, id = 139821571933952
thread2: 6
call thread1, id = 139821571933952
thread1: 7
call thread2 id = 139821580326656
call thread1, id = 139821571933952
thread1: 8
示例的解释:
call thread2:
- 是线程2即t_b首先上锁,即 pthread_mutex_lock(&mutex);
- 锁住了mutex使得此进程执行线程2中的临界区的代码,当执行到45行:if(i%3 != 0),此时i=1,满足此条件,则执行46行: pthread_cond_wait(&cond,&mutex);
- 这句是关键,pthread_cond_wait(&cond,&mutex)操作有两步,是原子操作:
- 第一 解锁,先解除之前的pthread_mutex_lock锁定的mutex;
- 第二 挂起,阻塞并在等待对列里休眠,即线程2挂起,直到再次被唤醒,唤醒的条件是由pthread_cond_signal(&cond);发出的cond信号来唤醒。
call thread1:
- 由于pthread_cond_wait已经对线程2解锁,此时另外的线程只有线程1,那么线程1对mutex上锁,
- 若这时有多个线程,那么线程间上锁的顺序和操作系统有关。
thread1: 1:
- 线程1上锁后执行临界区的代码,当执行到if(i%3 == 0)此时i=1,不满足条件,则pthread_cond_signal(&cond);
- 不被执行,那么线程2仍处于挂起状态,输出thread1: 1后线程1由pthread_mutex_unlock(&mutex);解锁。
thread1: 2: 这时此进程中只有2个线程,线程2处于挂起状态,那么只有线程1,则线程1又对mutex上锁,此时同样执行临界区的代码,而且i=2,不满足条件,pthread_cond_signal(&cond);不被执行,那么线程2仍处于挂起状态,输出thread1: 1后线程1由pthread_mutex_unlock(&mutex);解锁。
call thread1:同样由线程1上锁,但此时i=3,满足条件pthread_cond_signal(&cond)被执行,那么pthread_cond_signal(&cond)会发出信号,来唤醒处于挂起的线程2。pthread_cond_signal同样由两个原子操作:1,解锁;2,发送信号;解锁即对线程1解锁,解除对mutex的上锁。发送信号,即给等待signal挂起的线程2发送信号,唤醒挂起的线程2。
thread2: 3:由于pthread_cond_signal唤醒了线程2,即i=3满足条件,pthread_cond_wait(&cond,&mutex);被执行,那么pthread_cond_wait(&cond,&mutex)此时也有一步操作:上锁;即对线程2上锁,此时的pthread_cond_wait(&cond,&mutex)的操作相当与pthread_mutex_lock(&mutex);那么线程2继续执行上锁后的临界区的代码,并由pthread_mutex_unlock(&mutex);对线程2进行解锁。
剩下的输出原理和上面解释的一样。
纵观pthread_cond_wait,它的理解不可之把它看作一个简单的wait函数,它里面应该是一族函数,不同的函数在不同的条件下执行,理解pthread_cond_wait的机制可以很好的学习条件变量。