问题场景:
在实际程序处理中,经常会用到“触发某条件(认证、或判断数据的合法性)后,进行一系列的处理,然后将结果返回;若返回结果正确,则继续进行后面的处理,否则退出;一般等待结果有如下几种处理方式:
1)sleep() 死等,然后在 while(1) {} 中判断返回结果的状态; 若有实时性要求,还需要进行超时处理(超时也是处理失败);
2)在linux 多任务的系统中,sleep死等必然会导致系统运行实时性差;所以可以使用信号量、共享内存、或者消息队列等通信方式,让线程处于休眠等待状态。
3)也可以使用互斥量、条件变量;
条件变量、互斥锁——主要用于线程间通信
pthread_cond_wait()
pthread_cond_wait(&m_cond,&m_mutex); 指的是等待条件变量,总和一个互斥锁结合使用。
pthread_cond_wait() 函数执行时先自动释放指定的互斥锁,然后等待条件变量的变化;在函数调用返回之前(即wait成功获得cond条件的时候),会自动将指定的互斥量重新锁住(即在“等待的条件变量满足条件时,会重新锁住指定的锁”)。
pthread_cond_signal()
int pthread_cond_signal(pthread_cond_t * cond);
pthread_cond_signal()函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行(通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个)。如果没有线程处在阻塞等待状态, pthread_cond_signal() 也会成功返回(若唤醒线程时线程都处于工作状态,则在某线程当前任务执行结束后,线程从相应的队列中获取数据任务继续执行处理)。
pthread_cond_broadcast()可以唤醒所有等待线程,调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait() 的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal() 之后没有释放互斥锁,pthread_cond_wait() 仍然要阻塞。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求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()前的加锁动作对应。
为什么在唤醒线程后要重新mutex加锁?
pthread_cond_wait()函数运行全过程: 了解 pthread_cond_wait() 的作用非常重要:它是 POSIX 线程信号发送系统的核心,也是最难以理解的部分。
首先,让我们考虑以下情况:线程为查看已链接列表而锁定了互斥对象,然而该列表恰巧是空的。这一特定线程什么也干不了 -- 其设计意图是从列表中除去节点,但是现在却没有节点。因此,它只能:锁定互斥对象时,线程将调用 pthread_cond_wait(&mycond,&mymutex)。
pthread_cond_wait() 的作用:1)对互斥对象解锁(于是其它线程可以修改已链接列表),2)等待条件 mycond 发生(这样当 pthread_cond_wait() 接收到另一个线程的“信号”时,它将苏醒)。现在互斥对象已被解锁,其它线程可以访问和修改已链接列表,可能还会添加项。 【注意:解锁并阻塞是一个原子操作】
此时,pthread_cond_wait() 调用还未返回。对互斥对象解锁会立即发生,但等待条件 mycond 通常是一个阻塞操作,这意味着线程将睡眠,在它苏醒之前不会消耗 CPU 周期。这正是我们期待发生的情况:线程将一直睡眠,直到特定条件发生,在这期间不会发生任何浪费 CPU 时间的繁忙查询。从线程的角度来看,它只是在等待 pthread_cond_wait() 调用返回。
现在继续说明,假设另一个线程(称作“2 号线程”)锁定了 mymutex 并对已链接列表添加了一项。在对互斥对象解锁之后,2 号线程会立即调用函数 pthread_cond_broadcast(&mycond)。此操作之后,2 号线程将使所有等待 mycond 条件变量的线程立即苏醒。这意味着第一个线程(仍处于 pthread_cond_wait() 调用中)现在将苏醒。 是先解锁还是先signal,各有优缺点,下文会分析。另外注意,signal的函数里面是不是解锁加锁的,跟wait不一样。
现在,看一下第一个线程发生了什么。您可能会认为在 2 号线程调用 pthread_cond_broadcast(&mymutex) 之后,1 号线程的 pthread_cond_wait() 会立即返回。不是那样!实际上,pthread_cond_wait() 将执行最后一个操作:3)重新锁定 mymutex。一旦 pthread_cond_wait() 锁定了互斥对象,那么它将返回并允许 1 号线程继续执行。那时,它可以马上检查列表,查看它所感兴趣的更改。实际上,一般是先解锁。
来看一个例子:
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返回后会有原子加锁的 行为)(注:意思是说这时候signal的线程还没有unlock,所以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的线程),而这在上面的放中间的模式下是不会出现的。
相关博文:
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html
https://blog.csdn.net/zhou_xing_23/article/details/90898229
linux高级环境编程-线程 https://www.jianshu.com/p/51579476ad3e
pthread_cond_signal惊群现象 https://www.cnblogs.com/dodng/p/4380035.html
pthread_cond_signal频繁调用,会不会使pthread_cond_wait错过信号 https://bbs.csdn.net/topics/390351412
深入理解pthread_cond_wait、pthread_cond_signal