线程同步,条件变量pthread_cond_wait

混江龙づ霸主 提交于 2020-01-01 00:24:11

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,
主要包括两个动作:
一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。

pthread_cond_wait 原子调用: 等待条件变量, 解除锁, 然后阻塞 
当 pthread_cond_wait 返回,则条件变量有信号,同时上锁

等待条件有两种方式:
条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(), 
其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait() 
(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。

mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP), 
且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前, 
mutex保持锁定状态,并在线程挂起进入等待前解锁。 在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,
以与进入pthread_cond_wait()前的加锁动作对应。

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

 

1 简介
当多个线程之间因为存在某种依赖关系,导致只有当某个条件存在时,才可以执行某个线程,此时条件变量(pthread_cond_t)可以派上用场。
例1: 当系统不忙(这是一个条件)时,执行扫描文件状态的线程。
例2: 多个线程组成线程池,只有当任务队列中存在任务时,才用其中一个线程去执行这个任务。为避免惊群(thrundering herd),可以采用条件变量同步线程池中的线程。

 

2 用法
条件变量(pthread_cond_t)必须与锁(pthread_mutex_t)一起使用。

条件变量的API:

1) pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
使用 cond_attr 指定的属性初始化条件变量 cond,当 cond_attr 为 NULL 时,使用缺省的属性。LinuxThreads 实现条件变量不支持属性,
因此 cond_attr 参数实际被忽略。pthread_cond_t 类型的变量也可以用 PTHREAD_COND_INITIALIZER 常量进行静态初始化。

2) int pthread_cond_signal(pthread_cond_t *cond); / int pthread_cond_broadcast(pthread_cond_t *cond);
调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal之后没有释放互斥锁,pthread_cond_wait仍然要阻塞。
pthread_cond_signal使在条件变量上等待的线程中的一个线程重新开始。如果没有等待的线程,则什么也不做。如果有多个线程在等待该条件,只有一个能重启动。
在多处理器上,该函数是可能同时唤醒多个线程
pthread_cond_broadcast 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。

3) 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);
pthread_cond_wait 自动解锁互斥量(//如果不释放,其他线程永远改变不了条件,条件永不能为真,该线程将一直阻塞下去)(如同执行了 pthread_unlock_mutex),
并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。
pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。即条件变量触发--->(如果互斥锁被其他线程加锁,阻塞等到解锁)对互斥量加锁---->然后返回。(可能不是十分正确)
互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,
这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。

pthread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间
如果当前没有线程等待通知,则上面两种调用实际上成为一个空操作。

 

4) int pthread_cond_destroy(pthread_cond_t *cond);
销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。否则返回EBUSY
在 LinuxThreads 的实现中,条件变量不联结资源,除检查有没有等待的线程外,pthread_cond_destroy 实际上什么也不做。

 

3. 取消
pthread_cond_wait 和 pthread_cond_timedwait 是取消点。如果一个线程在这些函数上挂起时被取消,线程立即继续执行,
然后再次对 pthread_cond_wait 和 pthread_cond_timedwait 在 mutex 参数加锁,最后执行取消。因此,当调用清除处理程序时,可确保,mutex 是加锁的。


4. 异步信号安全(Async-signal Safety)

条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用
pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。

 

5 避免惊群
每当一块肉丢到狼群,就引发一群狼去争抢,但最后只有一只狼得到了肉。
计算机的惊群会造成服务器资源空耗。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.
如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
但使用pthread_cond_signal不会有“惊群现象”产生,它最多只给一个线程发信号。
假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。
如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。

 

6、pthread_cond_signal的位置
In Thread1:

pthread_mutex_lock(&m_mutex);
pthread_cond_wait(&m_cond,&m_mutex);
pthread_mutex_unlock(&m_mutex);

In Thread2:

pthread_mutex_lock(&m_mutex);
pthread_cond_signal(&m_cond);
pthread_mutex_unlock(&m_mutex);

 

为什么要与pthread_mutex 一起使用呢? 这是为了应对 线程1在调用pthread_cond_wait()但线程1还没有进入wait cond的状态的时候,此时线程2调用了 cond_singal 的情况。
如果不用mutex锁的话,这个cond_singal就丢失了。加了锁的情况是,线程2必须等到 mutex 被释放(也就是 pthread_cod_wait() 释放锁并进入wait_cond状态 ,此时线程2上锁)
的时候才能调用cond_singal.

pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有有缺点。

之间:
pthread_mutex_lock
xxxxxxx
pthread_cond_signal
pthread_mutex_unlock
缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的 行为),所以一来一回会有性能的问题。
但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列,
cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。
所以在Linux中推荐使用这种模式。

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

 

7、唤醒丢失问题
在线程并没有阻塞在条件变量上时,调用pthread_cond_signal或pthread_cond_broadcast函数可能会引起唤醒丢失问题。

唤醒丢失往往会在下面的情况下发生:

一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
没有线程正在处在阻塞等待的状态下。

 

-----------------------------------------------------------------------------

取消点的问题:

pthread_cond_wait 和pthread_cond_timedwait都被实现为取消点。因此,线程被取消之后,在该处等待的线程将立即重新运行,在重新锁定mutex后wait函数返回,然后执行取消动作。

也就是说,如果wait函数被取消,mutex将依然保持锁定状态,那么线程需要定义退出回调函数来为其解锁。

使用pthread_cleanup_push() 和 pthread_cleanup_pop设置回调函数保护。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!