我们知道,当调用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必须考虑伪唤醒的情况。下面的代码模拟了这种场景:

  1. #define COND_CHECK(func, cond, retv, errv) \
  2. if ( (cond) ) \
  3. { \
  4. fprintf(stderr, "\n[CHECK FAILED at %s:%d]\n| %s(...)=%d (%s)\n\n",\
  5. __FILE__,__LINE__,func,retv,strerror(errv)); \
  6. exit(EXIT_FAILURE); \
  7. }
  8.  
  9. #define ErrnoCheck(func,cond,retv) COND_CHECK(func, cond, retv, errno)
  10. #define PthreadCheck(func,rc) COND_CHECK(func,(rc!=0), rc, rc)
  11. #define FOREVER for(;;)
  12.  
  13. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  14. pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
  15. int predicate = 0;
  16. unsigned long nwakeup = 0; // number of wakeup
  17. unsigned long nspurious = 0; // number of spurious wakeup
  18.  
  19. /*****************************************************************************/
  20. /* thread - wait on condvar, do stats and reset the condvar */
  21. /*****************************************************************************/
  22. void*
  23. thread(void* ignore)
  24. {
  25. int rc;
  26.  
  27. FOREVER {
  28. // wait that predicate becomes true
  29. //
  30. rc = pthread_mutex_lock(&mutex);
  31. PthreadCheck("pthread_mutex_lock", rc);
  32. while (predicate==0) {
  33. rc = pthread_cond_wait(&cv, &mutex);
  34. PthreadCheck("pthread_cond_wait", rc);
  35. nwakeup++; // we've been wakeup
  36. if (predicate==0) nspurious++; // we got a spurious wakeup
  37. }
  38.  
  39. // reset predicate to false
  40. //
  41. predicate=0;
  42. rc = pthread_mutex_unlock(&mutex);
  43. PthreadCheck("pthread_mutex_unlock", rc);
  44. }
  45.  
  46. // never reached
  47. //
  48. pthread_exit(NULL);
  49. }
  50.  
  51. /*****************************************************************************/
  52. /* signal_thread - set predicate to true and signal the condvar */
  53. /*****************************************************************************/
  54. void*
  55. signal_thread(void* ignore)
  56. {
  57. int rc;
  58.  
  59. FOREVER {
  60. // set the predicate to true and wakeup one thread
  61. //
  62. rc = pthread_mutex_lock(&mutex);
  63. PthreadCheck("pthread_mutex_lock", rc);
  64. predicate=1;
  65. rc = pthread_mutex_unlock(&mutex); // unlock before signal
  66. PthreadCheck("pthread_mutex_unlock", rc);
  67. rc = pthread_cond_signal(&cv);
  68. PthreadCheck("pthread_cond_signal", rc);
  69. }
  70.  
  71. // never reached
  72. //
  73. pthread_exit(NULL);
  74. }
  75.  
  76. /*****************************************************************************/
  77. /* main- main thread */
  78. /*****************************************************************************/
  79.  
  80. const int NTHREADS = 8; // # threads waiting on the condvar
  81.  
  82. int
  83. main()
  84. {
  85. pthread_t tid[NTHREADS]; // threads waiting on the condvar
  86. pthread_t tsig; // thread that signals the condvar
  87. int rc; // return code
  88.  
  89. // create our threads
  90. //
  91. for (int i=0; i<NTHREADS; i++) {
  92. rc = pthread_create(tid+i, NULL, thread, NULL);
  93. PthreadCheck("pthread_create", rc);
  94. }
  95. rc = pthread_create(&tsig, NULL, signal_thread, NULL);
  96. PthreadCheck("pthread_create", rc);
  97.  
  98. // wait 3 sec, print statistics and exit
  99. //
  100. sleep(3);
  101. rc = pthread_mutex_lock(&mutex);
  102. PthreadCheck("pthread_mutex_lock", rc);
  103. printf("# wakeup = %8lu\n# spurious = %8lu (%2.2f%%)\n",
  104. nwakeup, nspurious, (float)nspurious/nwakeup*100.0
  105. );
  106. rc = pthread_mutex_unlock(&mutex);
  107. PthreadCheck("pthread_mutex_unlock", rc);
  108.  
  109. // that's all, folks!
  110. //
  111. return EXIT_SUCCESS;
  112. }

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

  1. # wakeup = 487936
  2. # 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的条件变量依然有效。比如下面的代码:

  1. #define COND_CHECK(func, cond, retv, errv) \
  2. if ( (cond) ) \
  3. { \
  4. fprintf(stderr, "\n[CHECK FAILED at %s:%d]\n| %s(...)=%d (%s)\n\n",\
  5. __FILE__,__LINE__,func,retv,strerror(errv)); \
  6. exit(EXIT_FAILURE); \
  7. }
  8.  
  9. #define ErrnoCheck(func,cond,retv) COND_CHECK(func, cond, retv, errno)
  10. #define PthreadCheck(func,rc) COND_CHECK(func,(rc!=0), rc, rc)
  11. #define FOREVER for(;;)
  12.  
  13. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  14. pthread_cond_t *ptr_cv;
  15. int predicate = 0;
  16. int nthreads;
  17.  
  18. /*****************************************************************************/
  19. /* thread - tell the shutdown thread that we're done */
  20. /*****************************************************************************/
  21. void*
  22. thread(void* ignore)
  23. {
  24. int rc;
  25.  
  26. // this thread now terminate
  27. //
  28. rc = pthread_mutex_lock(&mutex);
  29. PthreadCheck("pthread_mutex_lock", rc);
  30.  
  31. nthreads--; // we have one thread less in the pool
  32.  
  33. // note: we unlock first, and then signal
  34. //
  35. rc = pthread_mutex_unlock(&mutex);
  36. PthreadCheck("pthread_mutex_unlock", rc);
  37. rc = pthread_cond_signal(ptr_cv);
  38. PthreadCheck("pthread_cond_signal", rc);
  39.  
  40. // Ok, time to retire
  41. //
  42. pthread_exit(NULL);
  43. }
  44.  
  45. /*****************************************************************************/
  46. /* shutdown_thread- wait all threads in the pool to finish and clean-up */
  47. /* condvar */
  48. /*****************************************************************************/
  49. void*
  50. shutdown_thread(void* ignore)
  51. {
  52. int rc;
  53.  
  54. // wait as long as one thread in the pool is running
  55. //
  56. rc = pthread_mutex_lock(&mutex);
  57. PthreadCheck("pthread_mutex_lock", rc);
  58.  
  59. while (nthreads>0) {
  60. rc = pthread_cond_wait(ptr_cv, &mutex);
  61. PthreadCheck("pthread_cond_wait", rc);
  62. }
  63.  
  64. // all thread stopped running: we can destroy the condvar
  65. //
  66. rc = pthread_cond_destroy(ptr_cv);
  67. PthreadCheck("pthread_cond_destroy", rc);
  68. free(ptr_cv);
  69.  
  70. // unlock mutex, and bye!
  71. //
  72. rc = pthread_mutex_unlock(&mutex);
  73. PthreadCheck("pthread_mutex_unlock", rc);
  74. pthread_exit(NULL);
  75. }
  76.  
  77. /*****************************************************************************/
  78. /* main- main thread */
  79. /*****************************************************************************/
  80.  
  81. const int NTHREADS = 8; // # threads in the pool
  82.  
  83. int
  84. main()
  85. {
  86. pthread_t pool[NTHREADS]; // threads pool
  87. pthread_t tshd; // shutdown thread
  88. unsigned long count=0; // counter
  89. int rc; // return code
  90.  
  91. FOREVER {
  92.  
  93. // initialize condvar
  94. //
  95. nthreads=NTHREADS;
  96. ptr_cv = (pthread_cond_t*) malloc(sizeof(*ptr_cv));
  97. ErrnoCheck("malloc", (ptr_cv==NULL), 0);
  98. rc = pthread_cond_init(ptr_cv, NULL);
  99. PthreadCheck("pthread_cond_init", rc);
  100.  
  101. // create shutdown thread
  102. //
  103. rc = pthread_create(&tshd, NULL, shutdown_thread, NULL);
  104. PthreadCheck("pthread_create", rc);
  105.  
  106. // create threads pool
  107. //
  108. for (int i=0; i<NTHREADS; i++) {
  109. rc = pthread_create(pool+i, NULL, thread, NULL);
  110. PthreadCheck("pthread_create", rc);
  111. rc = pthread_detach(pool[i]);
  112. PthreadCheck("pthread_detach", rc);
  113. }
  114.  
  115. // wait shutdown thread completion
  116. //
  117. rc = pthread_join(tshd, NULL);
  118. PthreadCheck("pthread_join", rc);
  119.  
  120. // great... one more round
  121. //
  122. ++count;
  123. printf("%lu\n", count);
  124. }
  125.  
  126. // should be never reached
  127. //
  128. return EXIT_SUCCESS;
  129. }

上面的代码在运行时,会发生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. C/C++ - 类中成员变量是引用

    C++引用 引用在定义时必须初始化,否则编译时便会报错.如果类(自定义类型)的成员是引用类型,需要注意一些问题. 引用成员变量 并不为这个变量新辟空间:类对象做成员变量则是要对其新辟一段空间的 不能有 ...

  2. useradd -M -s /sbin/nologin mysql -g mysql 报错 Creating mailbox file

    由于之前使用以下命令删除了mysql账户 userdel mysql groupdel mysql      #如果删除了mysql用户,对应的组也会被删除(只有一个用户的情况下) 执行以下命令时报错 ...

  3. Lowest Common Ancestor (LCA)

    题目链接 In a rooted tree, the lowest common ancestor (or LCA for short) of two vertices u and v is defi ...

  4. LintCode_100 删除排序数组中的重复数字 ||

    题目 跟进“删除重复数字”: 如果可以允许出现两次重复将如何处理? 样例 给出数组A =[1,1,1,2,2,3],你的函数应该返回长度5,此时A=[1,1,2,2,3]. C++代码 int rem ...

  5. 如何确定要对DIV设置什么CSS属性样式呢?

    设置什么CSS样式不是凭空想象的而是有参考的,一般分三种情况下得到需要知道设置什么样式. 第一种:没有美工图,自己边思考布局 这种没有美工图也没有可参考的情况下DIV CSS布局,根据自己实际构思的想 ...

  6. Python实例 复制文件

    import  shutil import  os import  os.path src = " d:\\download\\test\\myfile1.txt " dst = ...

  7. 给iview项目加一个i18n国际化翻译

    看了上一篇博客吗?我们就根据那一篇博客来,用里面的项目,进行我们接下来国际化翻译项目. 我们安装vue-i18n和js-cookie npm install vue-i18n npm install ...

  8. [洛谷P4141] 消失之物「背包DP」

    暴力:暴力枚举少了哪个,下面套一个01背包 f[i][j]表示到了i物品,用了j容量的背包时的方案数,f[i][j]=f[i-1][j]+f[i-1][j-w[i]]O(n^3) 优化:不考虑消失的, ...

  9. poj 2001 Shortest Prefixes(字典树trie 动态分配内存)

    Shortest Prefixes Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 15610   Accepted: 673 ...

  10. 数据库Mysql监控及优化

    在做 性能测试的时候数据最重要,数据来源于哪里呢,当然是数据库了,数据库中,我们可以知道,数据从磁盘中要比从缓存中读取数据的时间要慢的多的多,还可以知道,同样的一个sql语句,执行的效率也不一样,这是 ...