条件变量用例--解锁与signal的顺序问题

摘要:
在单核系统上,如果在锁定状态下调用信号/广播,则可能导致不必要的上下文切换。考虑上图中的场景:T2在条件变量上阻塞,T1在持有锁时调用信号,然后上下文切换到T2,T2唤醒,但T2是_ cond_当等待返回时,需要重新锁定锁。然而,锁仍在T1手中。如果使用的Pthreads实现没有waitmorphing,我们可能需要在解锁后签名/广播。现在T3检查条件是否为真,处理它,并在T1执行信号/广播之前重置条件。

         我们知道,当调用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,这可能导致不必要的上下文切换。

条件变量用例--解锁与signal的顺序问题第1张

         考虑上图的场景: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, "
[CHECK FAILED at %s:%d]
| %s(...)=%d (%s)

",
              __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
# spurious = %8lu (%2.2f%%)
", 
          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, "
[CHECK FAILED at %s:%d]
| %s(...)=%d (%s)

",
              __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
", 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的顺序问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇第一章Android系统移植与驱动开发概述JUnit3 一次运行多个测试类和进行多次重复测试:使用测试套件和RepeatedTest下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

C#编程总结(二)多线程基础

C#编程总结(二)多线程基础 无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作。要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术。 多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时...

Qt的QWaitCondition(允许线程在一定条件下唤醒其他线程,这样对不间断上传可能比较适用)

  对生产者和消费者问题的另一个解决办法是使用QWaitCondition,它允许线程在一定条件下唤醒其他线程。其中wakeOne()函数在条件满足时随机唤醒一个等待线程,而wakeAll()函数则在条件满足时唤醒所有等待线程。   下面通过一个典型用例:生产者和消费者,来实现这二者之间的同步。整个工程就一个main.cpp,文件如下: #inclu...

InvokeRequired与Invoke

在多线程应用中将会涉及不同的线程访问同一控件的问题,C#中禁止跨线程直接访问控件。某个控件在被创建时就记下了是谁创建了它,即它的创建线程。如果从另一个线程调用该控件,那么必须使用控件的 Invoke 方法来将调用封送现在调用它的线程。(Invoke方法是控件的方法) 到底是哪个线程要使用该控件呢?需要用InvokeRequired来询问一下,如果当前调用线...

Linux线程同步之读写锁

1. 特性:     一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性, 当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞. 当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须阻塞知...

Jdk1.8 JUC源码解析(1)-atomic-AtomicXXX

目录     一、Unsafe简介 在正式的开讲 juc-atomic框架系列之前,有必要先来了解下Java中的Unsafe类。 Unsafe类,来源于sun.misc包。该类封装了许多类似指针操作,可以直接进行内存管理、操纵对象、阻塞/唤醒线程等操作。Java本身不直接支持指针的操作,所以这也是该类命名为Unsafe的原因之一。 J.U.C中的许多CA...

多线程调用有参数的方法---c# Thread 与 Task

  C#实现多线程的方式:Task——任务       简介   .NET 4包含新名称空间System.Threading.Tasks,它 包含的类抽象出了线程功能。 在后台使用ThreadPool。 任务表示应完成的某个单元的工作。 这个单元的工作可以在单独的线程中运行,也可以以同步方式启动一个任务,这需要等待主调线程。 使用任务不仅可以获得一个抽象...