pthread_cond_signal()和pthread_cond_wait()的典型使用方法
pthread_mutex_t mtx;
pthread_cond_t cond;
线程A中通知的执行的操作
线程B执行被唤醒的操作
在线程B中为什么把pthread_cond_wait()放在while循环中?很多人不能理解。大部分人认为把pthread_cond_wait()放在if中就行了。这是因为他们没有理解到,pthread_cond_wait()会苏醒的条件,在《POSIX多线程程序设计》中有很详细的说明,现简约摘录:
1. 被拦截的唤醒:也就是说可能有多个线程被唤醒(被pthread_cond_broadcast唤醒),被唤醒的线程都“争着”去处理共享数据,但只有一个线程能处理到这个共享数据,等它处理完毕后,其他线程也会进入该临界区去处理共享数据,这个时候就会出现共享数据被多次处理的情况,我们的解决方法是在被唤醒后,在去检查一下谓词变量,看该共享数据是否已经被处理了。
2. 松散的谓词:有时候谓词表示了“可能有工作”而不是“有工作”的意思。所以,当线程被唤醒,我们必须确认到底有没有工作。
3. 假唤醒:在有些系统中,我们没有用pthread_cond_signal()或pthread_cond_broadcast()来唤醒用pthread_cond_wait()休眠的线程,但是在休眠的线程还是会被唤醒,这是怎么回事呢?在Linux中,被休眠的进程或线程,都有被信号(linux中的信号)打断的可能,也就是说,如果线程或进程在休眠中,会有被唤醒的可能,所以我们在有可能引起休眠的API调用返回后都会检查一下返回值是否为EINT,如果是,那么线程或进程都将继续休眠。
为什么不把线程A中的pthread_cond_signal()放在pthread_mutex_unlock()前面?
当一个等待线程被唤醒的时候,它必须首先加锁互斥量(参见pthread_cond_wait()执行步骤)。如果线程被唤醒而此时通知线程任然锁住互斥量,则被唤醒线程会立刻阻塞在互斥量上,等待通知线程解锁该互斥量,引起线程的上下文切换。当通知线程解锁后,被唤醒线程继续获得锁,再一次的引起上下文切换。这样导致被唤醒线程不能顺利加锁,延长了加锁时间,加重了系统不必要的负担。
pthread_cond_wait()中为什么需要把mtx传递进去?
先考虑没有mtx参数的pthread_cond_wait(), 我们是否可以像下面这样等待“信号”的到来
pthread_mutex_lock(&mtx);
while(检查谓词)
pthread_cond_wait(&cond);
pthread_mutex_unlock(&mtx);
上面这种方式在休眠的时候,没有把互斥量释放掉,将会导致通知线程无法获得互斥量,导致死锁。那把pthread_cond_wait(&cond)放在pthread_mutex_lock(&mtx)之前又怎么样呢?
while(检查谓词)
pthread_cond_wait(&cond);
pthread_mutex_lock(&mtx);
pthread_mutex_unlock(&mtx);
看起来好像没什么问题。考虑下面这种情况:
T3:线程C发现a等于0,执行pthread_cond_wait(),线程休眠。
T1:a++,谓词改变。
T2:线程B发现a大于0了,执行pthread_mutex_lock(),由于此时线程A持有互斥量,线程B进入阻塞。
T4:线程A完成了共享数据的操作,执行pthread_cond_signal()唤醒所有等待的线程。
T5:线程C被唤醒,进入临界区操作共享数据,操作完毕后退出。
T6:线程B获得了互斥量,进入临界区操作共享数据,此处会产生错误,重复操作了共享数据!!
造成这种情况的原因是因为对a的访问没有互斥量的保护,导致线程C和线程B读到的a都是不相同的。所以,要把互斥量放在pthread_cond_wait()中,在还没休眠前,锁定a的状态不被其他线程修改。
我们来看看pthread_cond_wait()的源代码
首先锁住cond中的锁,然后释放掉作为参数传递进来的互斥量。和在POSIX线程-条件变量(一)说的一样。
那pthread_cond_wait()函数最开始的两个语句能不能交换呢?
lll_mutex_lock (cond->__data.__lock);
err = __pthread_mutex_unlock_usercnt (mutex, 0);
我们把 pthread_cond_signal()和pthread_cond_wait()的典型使用方法 这段中线程B执行
被唤醒的操作简化和抽象一下:
对mtx的操作简化为LOCK, UNLOCK;
对cond中锁的操作简化为__lock, __unlock.
括号中的操作代表了pthread_cond_wait()中锁的操作。
抽象的结果为(整个序列表示为M):
LOCK
{
① __lock
② UNLOCK
……….操作该cond的某些字段……
__unlock
Wait
LOCK
}
UNLOCK
交换①②顺序的结果为(整个序列表示为N)
LOCK
{
② UNLOCK
① __lock
……….操作该cond的某些字段……
__unlock
Wait
LOCK
}
UNLOCK
在N中,②①之间可能会被打断,比如通知线程在②①之间已经发出了“信号”,但N还没有在休眠队列上准备好;那么,当CPU切换到N中,继续执行①后面的语句时,将会造成N的永久等待(因为“信号”被丢失了)。
在M中就不会有这种情况发生了么?在M中①②之间也可能被打断,但当①执行之后,cond已经被锁定,通知线程在①②之间不可能发出“信号”(因为pthread_cond_signal()也需要锁定cond)。那么该”信号”只有在M完成了对cond的某些字段的更新(相当于把该线程挂入了休眠队列),释放了cond上的锁后,才有机会被发送。通过将解锁操作与等待条件变量原子化,确保了在释放互斥量和等待条件变量之间没有线程可以改变谓词。