随笔之POSIX cond和Windows同步对象Event的讨论

 一 缘由

最近在实现一个线程池的时候,需要用到POSIX中的cond和mutex进行线程间等待和同步,功能类似MS的同步对象Event。
发现cond和mutex的连用还是挺不人性化的。说实话,MS在同步对象的API上,做得还是相当不错,文档也很清晰。
Anyway,既然只能使用POSIX,就只能将就了。
我这个线程池在实现中碰到以下2个问题:
1 有n个线程等待一个事件。当有任务添加的时候,需要触发其中一个线程启动。
2 当线程池退出时,我需要触发所有线程启动,并检测退出标志,从而退出线程循环。
这个问题其实比初看上去要复杂,下面来分析
二 Windows上的实现
先介绍下Event同步对象,
HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes, // pointer to security attributes
  BOOL bManualReset,  // flag for manual-reset event
  BOOL bInitialState, // flag for initial state
  LPCTSTR lpName      // pointer to event-object name
);
第二个参数:bManualReset表示该对象为人工还是自动变量。我们重点就是讨论这个,该值决定以下几个特点:
1 当Event为人工变量时,一旦被触发,则一直保持触发状态,触发调用ResetEvent重置状态。假设该值被触发,那么调用WaitXXX函数的所有线程都不应该阻塞在该事件上。
2 当Event为自动变量时,一旦被触发,如果有线程在等,那么只会启动其中一个线程(只会启动一个线程,因为该值在启动一个线程后会重置)。如果没有线程在等,那么该值一直保持触发状态
从上面Event的介绍来看,自动变量一次触发一个线程,而人工变量一直保持触发状态。不过系统并没有说如果一个线程很快SetEvent并又ResetEvent的话,Wait的线程会如何。
 
我们先看看在MS平台上该如何解决上面的问题:
最普遍的方法就是:
1 创建一个Auto的Event,这个Event用来触发工作线程从任务队列中获取任务
2 创建一个Manual的Event,这个Event用来触发所有工作线程退出
然后利用WaitForMultiObject来等待这两个Event。
Problem solved!!
三 Linux的实现
Linux上最大的问题是没有WaitForMultiObject这样的函数,那么我们只能创建一个包含Mutex和Cond的结构体来充当Event
由于cond的触发有两个函数,特性分别是:
1 pthread_cond_signal:保证多个线程调用pthread_cond_wait等待的时候只有一个线程能够返回。
2 pthread_cond_broadcast:保证多个线程等待的时候,都能返回。
我们在使用cond实现类型的触发和等待的时候,代码经常如下所示:
void Wake(allThreads?)
{
   Mutex lock
   set condition = 1;
  if(allThreads)
    pthread_cond_broadcast  //触发所有线程
else
   pthread_cond_signal  //触发单个线程
   Mutex unlock
}
void Wait()
{
   Mutex lock
  while(condition != 1)
   {
      pthread_cond_wait(cond,&mutex); //cond的wait需要传入一个Mutex做参数,wait函数内部会unlock这个mutex
   }
   Mutex unlock
}
 
为什么在Wait的时候需要有一个while循环来检测condition是否满足条件呢?最主要有两个原因:
1 cond本身的机制导致。如果cond触发(不论是signal还是broadcast)的时候,没有线程在wait的话,那么线程以后wait将无法捕获这个触发。这和Windows的Event截然不同。自动变量的Event如果触发后,即使当时没有线程在等待,如果以后有线程等待的话,那么也是可以返回的。而posix的cond没有实现这个功能。所以我们只能自己加condition来做判断。如果condition为1,则无需等待。
2 pthread_cond_wait的返回有可能是因为信号的原因导致,这个时候conditon并不满足,所以这里也需要一个while循环
根据上面的基础知识,我们目前已满足的是:
1 cond_signal能并且只能触发一个线程起来
2 cond_broadcast能触发所有线程起来
但是问题随之而来,因为现在只有一个condition,什么时候置为零呢?必须满足两个条件:
1 对于触发单个线程的来说,因为只有一个启动,所以当它启动后,需要把condtion置为零,表示自己已经消费了这个条件。
2 对于所有启动的来说,只有最后一个线程退出等待的时候需要把conditon置为零,表示所有人都消费了这个条件。
解决办法就是使用waiter计数,最终修改后的代码如下:
void Wake(allThreads?)
{
   Mutex lock
   if(condtion == -1 || condition == 1)  //为什么要加这样的判断?
     {
       Mutex unlock
       return;
     }
    if(allThreads)
  {
    set condition = -1; //表示等待多个
     pthread_cond_broadcast  //触发所有线程
  }
else
{
   set condition = 1;
    pthread_cond_signal  //触发单个线程
}
   Mutex unlock
}
void Wait()
{
   Mutex lock
  ++waiters;
  while(condition == 0)
   {
      pthread_cond_wait(cond,&mutex); //cond的wait需要传入一个Mutex做参数,wait函数内部会unlock这个mutex
   }
  if(--waiters == 0)
     condition = 0;
 
   Mutex unlock
}
我们可以捋一下各种case:
1 只触发单个线程的话,没有问题。这个是最简单的。
2 触发多个线程的话,如果在触发后并且在旧的等待线程未全部返还之前,如果又有新线程调用wait的话,实际上是不会被阻塞的。
Wake函数前面红色字体的代码干什么用?
就怕调用者先调用了Wake(true),旋即又调用了Wake(false),导致condtion值混乱。通过判断condition是否已经触发,我们可以避免出问题。
 
四:结论
POSIX的cond和mutex联合使用,总感觉效率上会有损失。因为cond_wait返还后还是需要竞争mutex,实际上Wait函数在用户空间是串行执行的。
不知道Windows的Event是如何实现的?
上面的代码应该还不是很完善,有问题再修改,不知道您看出什么毛病了吗?
 
 
 
posted @ 2011-12-07 15:42  innost  阅读(2055)  评论(0编辑  收藏  举报