随笔-0  评论-0  文章-40  trackbacks-0

本文为个人分析的结论和现场回放,如有错误请各位不吝指出,欢迎基于问题本身的争论和讨论,但骂人者鄙人不收请悉数领回。


这个BUG如果说开了也并不复杂,不过也着实费了些功夫。 这案例是一个很好的警钟,再一次让我们明白,多线程代码并不是只要有了锁就万事大吉,恰恰相反,任何一点不够仔细或者考虑不到位都会导致很诡异的问题。(BTW: 个人对侯杰书中用CPP构造/析构特性封装临界区的做法很不赞同,因为这会导致程序员根本不去探究细节直接拿来就用,很多场合会导致很严重的问题,而且为优化带来了很大的麻烦)。

好了来看正题,描述如下:

有2个线程 PushThread和PopThread, 线程安全容器T, 非易失性标志量m_AsyncIOThreadExitFlag(初值为FALSE)。

PushThread将元素压入队列,PopThread将元素从队列中弹出并进行处理。当PushThread要退出时,它会压入最后一个待处理的元素并设置m_AsyncIOThreadExitFlag = TRUE。


PopThread将元素从队列中弹出进行适当处理,当m_AsyncIOThreadExitFlag为TRUE且T为空(Pop操作失败)时,PopThread退出守护循环,进而退出线程本身。

代码如下:


//存入线程PushThread在退出时的代码
VOID PushThread(VOID)
{
      VOID* lpWaitForCloseFileNode = GetLastFileNode();


      // 将最后一个元素压入队列
      PushToWaitForCloseQueue( lpWaitForCloseFileNode );


      // 设置退出标志位 通知PopThread退出
      m_AsyncIOThreadExitFlag = TRUE


      // 等待Pop线程退出并关闭该线程, INFIINITE表无限等待
      m_WaitForCloseFileThread.Release(INFINITE);  
}


//读取操作线程PopThread的代码
VOID PopThread(VOID)
{
      VOID* lpWaitForCloseFileNode;
      BOOL RetValue;

      while((RetValue = PopFileObjectFromWaitForCloseQueue(&lpWaitForCloseFileNode)) 
                  || !m_AsyncIOThreadExitFlag)
      {
          if (FALSE == RetValue)
                 {continue;}
          // do some process with lpWaitForCloseFileNode  
       }  
}


这两段代码都很简单对不对?而且我估计大部分人看在T是线程安全的份上应该就没多想(我当时就是,唉),实际上上述代码在一个线程切换非常频繁,线程数量繁多的进程中是不安全的(随着线程数量的上升,BUG发生比例同比增长)。下面我来描述下BUG发生时的精彩瞬间! :-)(请参考上面贴出的代码并按照我描述的过程稳步前进)

环境配置: 当前机器有4个处理器核心,最高并发4线程,分别为: P0 - P4。 Px:YY 表示有第x个处理器内核执行YY语句。

当P0:PopThread()执行完   PopFileObjectFromWaitForCloseQueue ,此时T为空,因此RetValue得到值FALSE。但还没有来得及执行  !m_AsyncIOThreadExitFlag 时,P0被切换掉执行其他的线程(是否切换为执行PushThread没有关系)

此时P1:PushThread   得以继续执行PushToWaitForCloseQueue( lpWaitForCloseFileNode ),因为T是线程安全的,因此在P0:PopThread()执行Pop操作时 P1:PushThread在等待获取锁。现在终于可以获取锁成功,于是P1:PushThread()执行Push操作完成,并继续向下执行        m_AsyncIOThreadExitFlag = TRUE (到这里PushThread()的主要工作已经完成,此后它爱怎么执行就怎么执行,已经无关紧要了)

此时系统在调度了几轮之后终于发现PopThread()很饥饿(因为自从上次被从P0上切下来之后还一直没有被切回去过),于是重新分配P0(其实分配哪个处理器内核不重要,考虑到系统内核会参考亲缘性,交给P0是比较典型的)。此时P0:PopThread()继续沿着断点向下执行,由于刚才P1:PushThread()已经设置了m_AsyncIOThreadExitFlag = TRUE,因此这里P0:PopThread()的下一个操作 !m_AsyncIOThreadExitFlag的结果为FALSE。好了我们来看一看这个时候P0:PopThread执行的这条完整语句吧(换行):

while((RetValue = PopFileObjectFromWaitForCloseQueue(&lpWaitForCloseFileNode)) 
                  || !m_AsyncIOThreadExitFlag)

此时RetValue在第一次P0:PopThread()时得值FALSE, !m_AsyncIOThreadExitFalg这个逻辑语句在第二次P0:PopThread()时得值FALSE(因为中间P1:PushThread()时设置了  m_AsyncIOThreadExitFalg = TRUE) ,于是这个while循环华丽的退出了。而实际上,正如我们在PushThread()的代码中看到的那样,此时队列中还有一个元素没有被弹出,它长存于队列中了。。。

 

上面就是BUG发生瞬间的精彩回放,可以看得出来,队列本身的状态和退出标志二者之间缺乏必要的同步机制是发生这个BUG的源头。解决方法有二:

方案一: 将对标志量的判断放入T的锁内,但是这样会增加T的耦合度,因为T只是个容器而已。

方案二: 在PushThread()中放入最后一个元素后,再保证没有其他线程Push元素的情况下,等待T为空之后,再设置退出标志。

考虑到现有代码的修改方便程度和耦合度,我最终选了方案二。 

posted on 2011-04-19 12:42 无毁湖光 阅读(473) 评论(0)  编辑 收藏 引用