线程学习九: 条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。  

1. 锁与条件变量之初始化

  • 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 
  • 动态初始化
int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr);//成功返回0
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);//成功返回0

2. 等待和激发

// 等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)   
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

// 激发
int pthread_cond_signal(pthread_cond_t * cond)
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait()
其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。  

2.1 等待实例

子线程中pthread_cond_wait()等待

/*
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求等待。
在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()) 
*/
pthread_mutex_t qlock;
pthread_cond_t  qready;
/************pthread_cond_wait()的使用方法**********/
pthread_mutex_lock(&qlock);    /*lock*/
//等待某资源,并以qready作为条件通知我们
pthread_cond_wait(&qready, &qlock); /*block-->unlock-->wait() return-->lock*/
//do something
pthread_mutex_unlock(&qlock); /*unlock*/
pthread_mutex_lock(&mtx);
//这个mutex_lock主要是用来保护wait等待临界资源,
//为何这里要有一个while (head == NULL)呢?
//因为如果有很多线程同时等待某资源,pthread_cond_wait里的线程可能会被意外唤醒,
//那么这个时候仍然head == NULL,这就是“惊群效应”
//这个时候,应该让线程继续进入pthread_cond_wait
while (head == NULL)  pthread_cond_wait(&cond, &mtx);
// pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx
//然后阻塞在等待队列里休眠,直到再次被唤醒
//(大多数情况下是等待的条件成立而被唤醒)
//唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx); 再读取资源
// 用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/
pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁

解析1

 1、pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait, while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程。
2、某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环. (惊群效应)

什么是惊群效应?
  有人觉得此处既然是被唤醒的,肯定是满足条件了,其实不然。如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时由于多处理器同时进行,也就是说,不同的线程要对同一个共享变量做操作。此时就必须要再次条件判断,以使只有一个线程进入临界区处理。
 
    其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。 

2.2 等待唤醒总结

pthread_cond_wait()用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。  pthread_cond_wait()必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过 pthread_cond_signal()或pthread_cond_broadcast把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex 。

pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。

使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续wait,

3. 等待和激发用法详解

* pthread_cond_wait必须放在pthread_mutex_lock和pthread_mutex_unlock之间,因为他要根据共享变量的状态来决定是否要等待,而为了不永远等待下去所以必须要在lock/unlock队中。共享变量的状态改变必须遵守lock/unlock的规则。
* pthread_cond_signal既可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有各缺点。

1.pthread_cond_signal放在pthread_mutex_lock和pthread_mutex_unlock之间

pthread_mutex_lock
xxxxxxx
pthread_cond_signal
pthread_mutex_unlock

缺点

    在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题,造成低效。

我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:
    1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。
    2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。
    3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。
    从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。

2.pthread_cond_signal放在pthread_mutex_lock和pthread_mutex_unlock之后

pthread_mutex_lock
xxxxxxx
pthread_mutex_unlock
pthread_cond_signal

优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了
缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。

所以,在Linux下最好pthread_cond_signal放中间,但从编程规则上说,其他两种都可以。

生产者——消费者模型例子代码:
https://cloud.tencent.com/developer/article/1629561

来自于
https://www.cnblogs.com/cthon/p/9084735.html
https://blog.csdn.net/u013187057/article/details/86549736

posted on 2022-05-12 17:11  JJ_S  阅读(157)  评论(0编辑  收藏  举报