我们知道,当调用signal/broadcast唤醒等待条件变量的其他线程时,既可以在加锁的情况下调用signal/broadcast,也可以在解锁的情况下调用。

那么,到底哪种情况更好呢?man手册中其实已经给出了答案:

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().

意思就是说,尽管既可以在持有锁的情况下调用signal/broadcast,也可以在解锁的情况下调用,但是如果需要调度行为是可预测的话,则应该在加锁的情况下调用signal/broadcast。

关于上述论点, 文章《Condvars: Signal With Mutex Locked Or Not?》(http://www.domaigne.com/blog/computing/condvars-signal-with-mutex-locked-or-not/)中做了详细解释,下面的描述主要翻译自该文章。

一:加锁时调用signal

某些平台上,在执行了signal/broadcast之后,为了减少延迟,操作系统会将上下文切换到被唤醒的线程。在单核系统上,如果在加锁的情况下调用signal/broadcast,这可能导致不必要的上下文切换。

考虑上图的场景:T2阻塞在条件变量上,T1在持有锁的情况下调用signal,接着上下文切换到T2,并且T2被唤醒,但是T2在从pthread_cond_wait返回时,需要重新加锁,然而此时锁还在T1手中。因此,T2只能继续阻塞(但是此时是阻塞在锁上),并且上下文又切换回T1。当T1解锁时,T2才得以继续运行。如果是调用broadcast唤醒等待条件变量的多个线程的话,那这种情形会变得更糟。

为了弥补这种缺陷,一些Pthreads的实现采用了一种叫做waitmorphing的优化措施,也就是当锁被持有时,直接将线程从条件变量队列移动到互斥锁队列,而无需上下文切换。

如果使用的Pthreads实现没有waitmorphing,我们可能需要在解锁之后在进行signal/broadcast。解锁操作并不会导致上下文切换到T2,因为T2是在条件变量上阻塞的。当T2被唤醒时,它发现锁已经解开了,从而可以对其加锁。

二:解锁后调用signal

解锁后调用signal有问题吗?首先,我们注意到,如果先进行signal/broadcast,则肯定会唤醒一个阻塞在条件变量上的线程;然而如果先解锁,则可能会唤醒一个阻塞在锁上的线程。

这种情形如何发生的呢?一个线程在锁上阻塞,是因为:

a:它要检查条件,并最终会在条件变量上wait;

b:它要改变条件,并最终通知那些等待条件变量的线程;

在a中,可能会发生唤醒截断的情况。重新考虑上图的场景,此时存在第三个线程T3阻塞在锁上。如果T1首先解锁,则上下文可能会切换到T3。现在T3检查到条件为真,进行处理,并在T1进行signal/broadcast之前,将条件重置。当T1进行signal/broadcast之后,T2被唤醒,而此时条件已经不再为真了。当然,在设计正确的应用中,这不是问题。因为T2必须考虑伪唤醒的情况。下面的代码模拟了这种场景:

#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 cv = PTHREAD_COND_INITIALIZER;
int predicate = 0;
unsigned long nwakeup = 0; // number of wakeup
unsigned long nspurious = 0; // number of spurious wakeup /*****************************************************************************/
/* thread - wait on condvar, do stats and reset the condvar */
/*****************************************************************************/
void*
thread(void* ignore)
{
int rc; FOREVER {
// wait that predicate becomes true
//
rc = pthread_mutex_lock(&mutex);
PthreadCheck("pthread_mutex_lock", rc);
while (predicate==0) {
rc = pthread_cond_wait(&cv, &mutex);
PthreadCheck("pthread_cond_wait", rc);
nwakeup++; // we've been wakeup
if (predicate==0) nspurious++; // we got a spurious wakeup
} // reset predicate to false
//
predicate=0;
rc = pthread_mutex_unlock(&mutex);
PthreadCheck("pthread_mutex_unlock", rc);
} // never reached
//
pthread_exit(NULL);
} /*****************************************************************************/
/* signal_thread - set predicate to true and signal the condvar */
/*****************************************************************************/
void*
signal_thread(void* ignore)
{
int rc; FOREVER {
// set the predicate to true and wakeup one thread
//
rc = pthread_mutex_lock(&mutex);
PthreadCheck("pthread_mutex_lock", rc);
predicate=1;
rc = pthread_mutex_unlock(&mutex); // unlock before signal
PthreadCheck("pthread_mutex_unlock", rc);
rc = pthread_cond_signal(&cv);
PthreadCheck("pthread_cond_signal", rc);
} // never reached
//
pthread_exit(NULL);
} /*****************************************************************************/
/* main- main thread */
/*****************************************************************************/ const int NTHREADS = 8; // # threads waiting on the condvar int
main()
{
pthread_t tid[NTHREADS]; // threads waiting on the condvar
pthread_t tsig; // thread that signals the condvar
int rc; // return code // create our threads
//
for (int i=0; i<NTHREADS; i++) {
rc = pthread_create(tid+i, NULL, thread, NULL);
PthreadCheck("pthread_create", rc);
}
rc = pthread_create(&tsig, NULL, signal_thread, NULL);
PthreadCheck("pthread_create", rc); // wait 3 sec, print statistics and exit
//
sleep(3);
rc = pthread_mutex_lock(&mutex);
PthreadCheck("pthread_mutex_lock", rc);
printf("# wakeup = %8lu\n# spurious = %8lu (%2.2f%%)\n",
nwakeup, nspurious, (float)nspurious/nwakeup*100.0
);
rc = pthread_mutex_unlock(&mutex);
PthreadCheck("pthread_mutex_unlock", rc); // that's all, folks!
//
return EXIT_SUCCESS;
}

上面的代码中,使用nwakeup记录pthread_cond_wait被唤醒的次数,用nspurious记录伪唤醒的次数。运行结果如下:

# wakeup   =   487936
# spurious = 215469 (44.16%)

可见伪唤醒的占比要在40%左右。(其实,采用先signal/broadcast,后unlock的写法,也依然会发生这种情况(亲测))

在b中,会推迟唤醒线程T2的时间。第三个线程T3阻塞在锁上,T1解锁后,T3得以继续执行。此时,只要T1不被调度,则它没有机会进行signal/broadcast,因此线程T2会一直阻塞。

三:实时的情况

在实时性的程序中,线程的优先级反映了线程deadline的重要性。粗略的说,deadline越重要,则优先级应该越高。如果无法满足deadline的要求,则系统可能会失败、崩溃。

因此,你肯定希望高优先级的线程能尽可能早的获取CPU得以执行,然而,有可能会发生优先级反转的情况,也就是低优先级的线程阻碍了高优先级线程的执行。比如锁被低优先级的线程持有,使得高优先级的线程无法加锁。实际上,只要优先级反转的时间是有界且较短的话,这种情况不会造成太大问题。然而当反转时间变得无界时,这种情况就比较严重了,这会导致高优先级的线程无法满足其deadline。

当采用实时调度策略时,signal/broadcast会唤醒高优先级的线程。如果多个线程具有相同的优先级,则先在条件变量上阻塞的线程会被唤醒。

在线程进行signal/broadcast之前,也可能会发生优先级反转。继续考虑上图的场景:T1是个低优先级(P1)的线程,T2是高优先级(P2)的线程,T3的优先级(P3)介于T1和T2之间:P1 < P3 < P2。

如果T1先进行unlock,则其在unlock和signal/broadcast之间,T1可能被更高优先级的T3抢占,从而T1无法唤醒T2,因此低优先级的T3阻碍了高优先级的T2的运行,发生了优先级反转。

如果T1先进行signal/broadcast,假设锁使用了优先级天花板或继承协议(参考《Programming.With.Posix.Threads》第5.5.5.1节和5.5.5.2节),则可以保证T1在解锁后,T2会立即被调度。

因此,当持有锁时进行signal/broadcast更具优势。基于上面的讨论,在实时调度中,先signal/broadcast后unlock是必须的……。

四:陷阱

如果先解锁,则可能会导致另一种问题:你必须保证解锁之后,用于signal/broadcast的条件变量依然有效。比如下面的代码:

#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;
}

上面的代码在运行时,会发生Segmentationfault。

五:结论

我个人倾向于,在持有锁的情况下进行signal/broadcast。首先,这样做可以避免隐蔽的bug;然后,在使用了wait morphing优化的Pthreads实现中,这样做几乎没有性能损耗;其次,我认为只有在明确表明性能可以得到显著提升时,才有必要先unlock,后signal/broadcast,优化那些并非导致性能瓶颈的点,是没有必要的。

原文:

http://www.domaigne.com/blog/computing/condvars-signal-with-mutex-locked-or-not/

条件变量用例--解锁与signal的顺序问题的更多相关文章

  1. Linux多线程实践(8) --Posix条件变量解决生产者消费者问题

    Posix条件变量 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); int pthread_co ...

  2. POSIX条件变量

    条件变量: 当一个线程互斥的访问某个变量时,它可能发现其他线程改变状态之前,它什么都做不了例如:一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中,这种情况就需要使用条件 ...

  3. pthread中互斥量,锁和条件变量

    互斥量 #include <pthread.h> pthread_mutex_t mutex=PTHREAD_MUTEX_INTIIALIZER; int pthread_mutex_in ...

  4. 条件变量signal与unlock的顺序

    编写同步队列时,有用到条件变量,对操作队列的线程进行同步.当队列为空时,允许get线程挂起,直到add线程向队列添加元素并通过唤醒条件变量,get线程继续向下运行.条件变量在多线程程序中用来实现“等待 ...

  5. 条件变量pthread_cond_t怎么用

    #include <pthread.h> #include <stdio.h> #include <stdlib.h> pthread_mutex_t mutex ...

  6. Linux多线程同步之相互排斥量和条件变量

    1. 什么是相互排斥量 相互排斥量从本质上说是一把锁,在訪问共享资源前对相互排斥量进行加锁,在訪问完毕后释放相互排斥量上的锁. 对相互排斥量进行加锁以后,不论什么其它试图再次对相互排斥量加锁的线程将会 ...

  7. 线程同步,条件变量pthread_cond_wait

    与互斥锁不同,条件变量是用来等待而不是用来上锁的.条件变量用来自动阻塞一个线程,直到某特殊情况发生为止.条件变量使我们可以睡眠等待某种条件出现.条件变量是利用线程间共享的全局变量进行同步的一种机制,主 ...

  8. 互斥量、条件变量与pthread_cond_wait()函数的使用,详解(一)

    1. 首先pthread_cond_wait 的定义是这样的 The pthread_cond_wait() and pthread_cond_timedwait() functions are us ...

  9. 条件变量pthread_cond_wait()和pthread_cond_signal()详解

    条件变量        条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起:另一个线程使"条件成立" ...

随机推荐

  1. CF538G (套路题)

    CF538G 题目大意 你有一个长度为\(l\)的指令序列,每个指令为向上,向下,向左,向右中的一种. 机器人会循环执行该序列,即,执行完第\(l\)个指令后,就会重新开始执行第一个指令. 现在,给你 ...

  2. C++类中的枚举类型

    在看effective c++的时候,其中第二条边指出.尽量使用const ,enum代替define.在写程序的时候,需要入参为设备类型,第一反应是枚举一个设备类型,并以名字命名.但是有一个问题挺困 ...

  3. 007-使用python统计代码行数,空行以及注释

    # 自己写过的程序,统计一下你写过多少行代码.包括空行和注释,但是要分别列出来 1.打开文件方法 1.1 以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和标示符 f ...

  4. MacBook下为要运行的.net core 项目指定sdk版本

    安装完.net core 3.0,运行早期版本构建的项目遇到运行错误,查阅官方文档解决问题,特此记录!官方原文如下: SDK 使用最新安装的版本 SDK 命令包括 dotnet new 和 dotne ...

  5. 2019.9.27 csp-s模拟测试53 反思总结

    这个起名方式居然还有后续?! 为什么起名不是连续的?! T1想了半天,搞出来了,结果数组开小[其实是没注意范围].T2概率期望直接跳,后来翻回来写发现自己整个理解错了期望的含义[何].T3错误想到赛道 ...

  6. spring和mybatis整合遇到org.springframework.beans.factory.BeanDefinitionStoreException

    今天对spring和mybatis整合进行练习,通过MapperScannerConfigurer进行mapper扫描 但是在进行单元测试的时候,死活就是报错,具体报错如下: org.springfr ...

  7. 移动端h5禁用浏览器左滑右滑的前进后退功能

    在项目运行过程中发现,用户在有左右滑动前进后退的功能的浏览器上签字时,偶然触发了前进后退会导致canvas像是重置了一样内容消失,所以需要在代码中处理这种情况. 基本原理就是在touchmove事件中 ...

  8. Ubuntu 安装 setuptools

    Setuptools的官方页面 Easily download, build, install, upgrade, and uninstall Python packages 它是一个对python的 ...

  9. 分析ajax请求过程以及请求方法

    ajax 的全称是Asynchronous JavaScript and XML,其中,Asynchronous 是异步的意思,它有别于传统web开发中采用的同步的方式.据小编翻墙了解到,ajax很早 ...

  10. 使用Redis管道提升性能

    首发于 樊浩柏科学院 Redis 的 管道 (pipelining)是用来打包多条无关命令批量执行,以减少多个命令分别执行带来的网络交互时间.在一些批量操作数据的场景,使用管道可以显著提升 Redis ...