Chinaunix首页 | 论坛 | 博客
  • 博客访问: 87326
  • 博文数量: 12
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 200
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 09:57
文章分类

全部博文(12)

文章存档

2015年(11)

2014年(1)

我的朋友

分类: LINUX

2015-09-04 08:55:01

    对于互斥量和条件变量的使用,很多书上早已给出了标准的使用方式,这里以SSDB中的Queue为例:

点击(此处)折叠或打开

  1. template <class T>
  2. // 通知方
  3. int Queue<T>::push(const T item){
  4.     // 1. 加锁 lock
  5.     if(pthread_mutex_lock(&mutex) != 0){
  6.         return -1;
  7.     }
  8.     // 2. 修改条件
  9.     {
  10.         items.push(item);
  11.     }
  12.     // 3. 解锁 unlock
  13.     pthread_mutex_unlock(&mutex);
  14.     // 4. signal通知
  15.     pthread_cond_signal(&cond);
  16.     return 1;
  17. }

  18. template <class T>
  19. // 等待方
  20. int Queue<T>::pop(T *data){
  21.     // 1. 加锁 lock
  22.     if(pthread_mutex_lock(&mutex) != 0){
  23.         return -1;
  24.     }
  25.     {
  26.         // 2. 循环检查条件
  27.         while(items.empty()){
  28.             // 3. wait 等待条件变化
  29.             if(pthread_cond_wait(&cond, &mutex) != 0){
  30.                 return -1;
  31.             }
  32.         }
  33.         *data = items.front();
  34.         items.pop();
  35.     }
  36.     // 4. 解锁 unlock
  37.     if(pthread_mutex_unlock(&mutex) != 0){
  38.         return -1;
  39.     }
  40.     return 1;
  41. }
    总结起来,等待方和通知方大体上就是上面的四步操作,对此也没有多想,直到有一天,Muduo作者陈硕发了一条微博  。
    
   初看起来,没觉得红框处有什么问题,这样封装相比起标准方法就是没有用循环检查条件是否满足,说不定这段代码的作者想在wait函数的外层用循环检查了,但是问题就在这里,即使在wait的外层用循环去检查也是错的,因为在这个wait函数的结尾已经将锁释放了,在不持有锁的情况下去检查条件是否满足这是完全错误的。因此这个wait函数是没法用的,它就是错误的。这里补充一点儿pthread_cond_wait的知识:
    pthread_cond_wait函数会原子的将调用线程放到等待队列中并释放互斥量,这样其他的线程就可以对互斥量加锁修改共享数据,然后调用pthread_cond_signal / phthread_cond_broadcast并解锁(两者的顺序无关紧要),这时pthread_cond_wait函数会获取锁并把线程从等待队列中拉出来,然后pthread_cond_wait函数返回。其实从道理上来说,对于共享数据,读写访问都是要加锁的,上面的wait函数释放了锁是不可能正确的检查条件的。

    这应该就是原Po说的意思,微博发出后,大家讨论的很热烈,很快大家就提出了自己的疑问:
    1. 这段代码是在模拟Windows下的Event,wait函数返回就说明事件被触发了。
        很可惜,这样也是错的,因为事件机制隐含的条件是wait和signal的执行有先后顺序,一定是等待方先wait了之后,通知方才能signal,但是上面的代码没有采取任何措施保证wait先于signal,如果signal先执行,再来wait,那事件就永远不会被捕获到了,而调用wait的线程会一直等待。
        因此,正确的模拟Event的代码应该是下面这样:利用一个signaled_来判断是否有事件发生,即使先调用了signal,wait函数也不会等待   

点击(此处)折叠或打开

  1. // Version 7: broadcast to wakeup multiple waiting threads
  2. // Probably the best version among above.
  3. class Waiter7 : public Waiter
  4. {
  5.  public:
  6.   void wait() override
  7.   {
  8.     pthread_mutex_lock(&mutex_);
  9.     while (!signaled_)
  10.     {
  11.       pthread_cond_wait(&cond_, &mutex_);
  12.     }
  13.     pthread_mutex_unlock(&mutex_);
  14.   }

  15.   void signal() override // Sorry, bad name in base class, poor OOP
  16.   {
  17.     broadcast();
  18.   }

  19.   void broadcast()
  20.   {
  21.     pthread_mutex_lock(&mutex_);
  22.     pthread_cond_broadcast(&cond_);
  23.     signaled_ = true;
  24.     pthread_mutex_unlock(&mutex_);
  25.   }

  26.  private:
  27.   bool signaled_ = false;
  28. };
    下面这两句话取自陈硕的文章: http://www.cppblog.com/Solstice/archive/2013/09/09/203094.html
    总结:使用条件变量,调用 signal() 的时候无法知道是否已经有线程等待在 wait() 上。因此一般总是要先修改“条件”,使其为 true,再调用 signal();这样 wait 线程先检查“条件”,只有当条件不成立时才去 wait(),避免了丢事件的可能。换言之,通过使用“条件”,将边沿触发(edge trigger)改为电平触发(level trigger)。这里“修改条件”和“检查条件”都必须在 mutex 保护下进行,而且这个 mutex 必须用于配合 wait()。
     style="font-family:Verdana, Geneva, Arial, Helvetica, sans-serif;font-size:16px;line-height:normal;white-space:normal;"> 这篇帖子里对 spurious wakeup 的解释是错的,spurious wakeup 指的是一次 signal() 调用唤醒两个或以上 wait()ing 的线程,或者没有调用 signal() 却有线程从 wait() 返回。manpage 里对 Pthreads 系列函数的介绍非常到位,值得细读。


    2. pthread_cond_signal / pthread_cond_broadcast 和 pthread_mutex_unlock的顺序问题:
    

点击(此处)折叠或打开

  1. template <class T>
  2. int Queue<T>::push(const T item){
  3.     if(pthread_mutex_lock(&mutex) != 0){
  4.         return -1;
  5.     }
  6.     {
  7.         items.push(item);
  8.     }
  9.     // signal是否需要放到unlock之前 ??
  10.     pthread_mutex_unlock(&mutex);
  11.     pthread_cond_signal(&cond);

  12.     return 1;
  13. }
    实际上,这两者的顺序是没有要求的,唯一有要求的就是修改共享条件,状态必须在加锁情况下,至于 pthread_cond_signal / pthread_cond_broadcast 是否需要加锁,完全没要求,在TLPI(The Linux Programming Interface)中,有详细的解释。

        

阅读(1593) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~