我们经常需要通过某种条件去唤醒和阻塞一个线程,我们唤醒线程时是否需要继续持有锁呢,换句话说我们是先释放锁再唤醒还是先唤醒再释放锁呢?
pthread_mutex_lock(&mutex);
predicate=true;
pthread_cond_signal(&cv); // OR: pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex); // : pthread_cond_signal(&cv);
权威的答案是:
The pthread_cond_broadcast() or pthread_cond_signal() functions may be called by a thread whether or not it currently owns the mutex that threads calling pthread_cond_wait() or pthread_cond_timedwait() have associated with the condition variable during their waits; however, if predictable scheduling behavior is required, then that mutex shall be locked by the thread calling pthread_cond_broadcast() or pthread_cond_signal().
大概意思就是pthread_cond_broadcast() or pthread_cond_signal()在被调用时可能持有锁也可能没有,但是如果你需要保证调度的条件是可预测的,那么你就需要持有共享的条件变量了。
到底什么意思呢?首先我们来看持有锁的情况。
这个时候有一个问题,我们知道cpu在扮演了调度线程的角色,在切换线程的过程中cpu需要保存线程有关的上下文并且恢复另外一个线程的上下文,即上下文切换。这也是问题所在。如果我们唤醒的时候持有锁,在一些单核的系统中会造成不必要的上下文切换。
看下面的情景,线程T2阻塞在条件变量上,线程T1调用signal函数唤醒线程T1,注意此时T2持有条件变量,此时cpu切换上下文到T2线程,T2唤醒,但是在T2返回pthread_cond_wait()之前T2线程需要lock条件变量,此时却发现条件变量仍然被T1T1锁着,cpu不得不再次切换到T1上下文,T1解锁互斥变量,然后再切换到,至此整个过程才算结束。当我们调用pthread_cond_broadcast()时,情况会更加的糟糕。
在有一些系统中线程的实现,用了一种叫做wait morphing的优化,当锁锁住条件变量时,它允许线程从条件变量队列切换到锁队列且不用切换上下文。但是如果我们的线程没有这种优化,我们就会为了减少上下文的切换,而先解锁条件变量,然后再signal/broadcast,此时切换到T2线程时直接就可以获取条件变量。但是这样真的就没有任何问题吗?如果我们先signal/broadcast,我们可以确保唤醒一个阻塞在条件变量的线程,但是如果我们先释放条件变量 ,那么我们就不能保证了。
仍然是图中的场景,不过此时我们有另外一个线程T3也阻塞在条件变量上(T3并不需要被signal/broadcast),如果T1解锁条件变量,cpu可能会切换到T3,这是T3看到条件为true,此时T3running,并在T1signal之前把条件重置为false,等到T1 signalT2唤醒时却发现条件为false,自己被骗了。这就是因为T1先释放了条件变量,然后再signal造成的。这也是我们为什么在wait线程当被唤醒时要再次判断唤醒条件了。比如:
while (唤醒条件) {
pthread_cond_wait(&cond, &mut);
}
Do something
当然这样写不光是为了防止这种情况,它同时也是为了防止当有多个线程在等待同一个条件变量时,在多处理器的系统中,有一个线程之外的线程被意外唤醒,而我们只需要一个线程被唤醒,这是while循环就可以保证只有一个线程被唤醒。
下面再看在一个实时系统中的情况,在实时系统中我们会要求优先级高的先于优先级低的执行,但是如果我们优先级高的线程尝试去lock已经被一个低优先级线程持有的条件变量,就会失败,当然有时候这也不会有什么问题,只要在下次尝试时成功即可,可是如果尝试多次都失败,线程极有可能错过自己的deadline。为了解决这个问题也有关的协议:优先级反转和优先级继承 点击打开链接。
仍然拿开始的图做例子,我们也有第三个线程T3(T3并不需要被signal/broadcast),优先级依次为T1<T3<T2,在T1signal之前它可能被T3枪占,比如如果我们先unlock,在T1unlock和signal之间,极有可能发生。最终会导致T2没有被唤醒,间接地T3阻止了较高优先级T2的执行。但是如果我们先signal,当T1unlock时由于T3优先级高于T2,T3就会立马执行。 另外当我们的signal/broadcast线程signal时,如果有多个同等优先级的线程在wait,我们希望先阻塞在条件变量的线程优先被唤醒,但是在有些情况下可能并不会如我们期望的那样。首先类似于上面的情况,如果最后阻塞的线程先持有锁那么导致最先阻塞的线程不会立刻被唤醒。
最后还有一个问题就是如果你在singal先unlock,你得确保你在signal时条件变量仍然有效,否则就会造成严重的后果。
/*------------------------------ cv_02.c --------------------------------*
On Linux, compile with:
cc -std=c99 -pthread cv_02.c -o cv_02
*--------------------------------------------------------------------------*/
#define _POSIX_C_SOURCE 200112L // use IEEE 1003.1-2004
#include <pthread.h>
#include <unistd.h> // sleep()
#include <stdio.h>
#include <stdlib.h> // EXIT_SUCCESS
#include <string.h> // strerror()
#include <errno.h>
/***************************************************************************/
/* our macro for errors checking */
/***************************************************************************/
#define COND_CHECK(func, cond, retv, errv) \
if ( (cond) ) \
{ \
fprintf(stderr, "\n[CHECK FAILED at %s:%d]\n| %s(...)=%d (%s)\n\n",\
__FILE__,__LINE__,func,retv,strerror(errv)); \
exit(EXIT_FAILURE); \
}
#define ErrnoCheck(func,cond,retv) COND_CHECK(func, cond, retv, errno)
#define PthreadCheck(func,rc) COND_CHECK(func,(rc!=0), rc, rc)
#define FOREVER for(;;)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t *ptr_cv;
int predicate = 0;
int nthreads;
/*****************************************************************************/
/* thread - tell the shutdown thread that we're done */
/*****************************************************************************/
void*
thread(void* ignore)
{
int rc;
// this thread now terminate
//
rc = pthread_mutex_lock(&mutex);
PthreadCheck("pthread_mutex_lock", rc);
nthreads--; // we have one thread less in the pool
// note: we unlock first, and then signal
//
rc = pthread_mutex_unlock(&mutex);
PthreadCheck("pthread_mutex_unlock", rc);
rc = pthread_cond_signal(ptr_cv);
PthreadCheck("pthread_cond_signal", rc);
// Ok, time to retire
//
pthread_exit(NULL);
}
/*****************************************************************************/
/* shutdown_thread- wait all threads in the pool to finish and clean-up */
/* condvar */
/*****************************************************************************/
void*
shutdown_thread(void* ignore)
{
int rc;
// wait as long as one thread in the pool is running
//
rc = pthread_mutex_lock(&mutex);
PthreadCheck("pthread_mutex_lock", rc);
while (nthreads>0) {
rc = pthread_cond_wait(ptr_cv, &mutex);
PthreadCheck("pthread_cond_wait", rc);
}
// all thread stopped running: we can destroy the condvar
//
rc = pthread_cond_destroy(ptr_cv);
PthreadCheck("pthread_cond_destroy", rc);
free(ptr_cv);
// unlock mutex, and bye!
//
rc = pthread_mutex_unlock(&mutex);
PthreadCheck("pthread_mutex_unlock", rc);
pthread_exit(NULL);
}
/*****************************************************************************/
/* main- main thread */
/*****************************************************************************/
const int NTHREADS = 8; // # threads in the pool
int
main()
{
pthread_t pool[NTHREADS]; // threads pool
pthread_t tshd; // shutdown thread
unsigned long count=0; // counter
int rc; // return code
FOREVER {
// initialize condvar
//
nthreads=NTHREADS;
ptr_cv = (pthread_cond_t*) malloc(sizeof(*ptr_cv));
ErrnoCheck("malloc", (ptr_cv==NULL), 0);
rc = pthread_cond_init(ptr_cv, NULL);
PthreadCheck("pthread_cond_init", rc);
// create shutdown thread
//
rc = pthread_create(&tshd, NULL, shutdown_thread, NULL);
PthreadCheck("pthread_create", rc);
// create threads pool
//
for (int i=0; i<NTHREADS; i++) {
rc = pthread_create(pool+i, NULL, thread, NULL);
PthreadCheck("pthread_create", rc);
rc = pthread_detach(pool[i]);
PthreadCheck("pthread_detach", rc);
}
// wait shutdown thread completion
//
rc = pthread_join(tshd, NULL);
PthreadCheck("pthread_join", rc);
// great... one more round
//
++count;
printf("%lu\n", count);
}
// should be never reached
//
return EXIT_SUCCESS;
}
上面的程序在运行一段时间后就会崩溃,因为我们在线程数为0的时候释放了条件变量,而在signal线程中对一个不存在的条件变量发出signal,如果我们先signal在unlock那么程序就会正常运行了。
因此,对于程序而言,我偏向于先signal后unlock,Signal With Mutex Locked可以保证我们程序的逻辑按照我们预期的执行,可以避免一些很隐秘的bug,当然如果你是个性能癖,你能保证在unlock和signal之间不会发生一些意外的超出你掌控事情你可以先unlock在signal,这样做同样也不会有问题。