如何写优雅的代码(5)——远离临界区噩梦

//========================================================================
//TITLE:
//    如何写优雅的代码(5)--远离临界区噩梦
//AUTHOR:
//    norains
//DATE:
//    Tuesday 01- December-2009
//Environment:
//    WINDOWS CE 5.0
//========================================================================

  在平时的多线程编程中,我们往往离不开临界区。会用,甚至是用得好的朋友,估计不少;但用得万无一失的,相对而言,可能就会少很多。
  
  不信?我们来看看下面这段代码:  
  

 

  这段代码看起来似乎没有什么问题?但如果说,这段代码会引发孤立临界区的问题,你会信么?换句话说,如果Cal3rdPartCode(pParam)函数会抛出异常,那么代码就直接跳转到catch部分。那事情就严重了,因为代码根本就没有调用LeaveCriticalSection(&g_cs),将g_cs这临界区完全孤立了起来,别的线程就只能一直等待该临界区。
  
  似乎情况很糟糕,不是么?当然,糟糕并不代表陷入世界末日,虽然路途上充满了荆棘,但毕竟我们还是能看到阳光。
  
  我们总不能在一棵树上吊死吧。换个角度,临界区的使用,必须是EnterCriticalSection和LeaveCriticalSection配套使用。那么我们想想,C++里面,有什么东西是强制配套的,根本不用使用者干预的?也许有的朋友已经想到了。没错,是类,类的构造函数和析构函数。
  
  正常使用的话,无论使用者愿不愿意,类的构造函数和析构函数都是被编译器强制配套使用。所以,我们可以在构造函数中调用EnterCriticalSection,在析构函数则是LeaveCriticalSection。
  
  根据此思想,我们最简单的用类来调用临界区的代码诞生了:
  


  使用上也是简单明了:
  

  但这样完美了么?很遗憾,这只是万里长征的第一步,我们的路还很漫长。最根本的问题,我们如何确保传入的临界区指针是有效的?换句话说,我们无法保证传入的临界区指针是没有调用过DeleteCriticalSection的。
  
  如果代码是如下的书写,很明显,肯定会有我们无法预料的异常:
   
    
  那有没有这么一种机制,我们可以通过它进入临界区,但我们却不能让它删除临界区,或是即使删除了,我们也有足够的信息获知。
  
  万事无绝对,既然我们能想到,那么我们就能做到。人有多大胆,地有多大产,在这个奇迹倍出的IT界,有时候也并不只是一句华而不实的口号。
  
  首先,我们将创建临界区的操作封装于类中,使用者只能获得创建的序号;然后,如果想进入临界区,只要传入序号即可;最后,如果不需要该临界区,那么传入序号删除即可。这样的好处是,通过序号这个桥梁,既能使用临界区的功能,又不会引发临界区的不稳定。
  
  我们先来看看创建的函数:
   
    
  虽然我们将Create函数封装于CLock类中,但我们不打算需要通过对象才能调用,所以我们以static进行修饰,让其成为类函数。那么,我们调用的时候,就可以直接通过类作用域使用:
  
    
  
  我们留意一下这句:
   
       
  这句代码用来在栈中创建CRITICAL_SECTION对象。之所以这么做,是因为如果使用局部变量,那么在函数返回时,局部变量就被删除。而通过new创建出来的对象,除非我们调用delete,否则在程序运行期中一直存在。这样就能达到创建的目的。
  
  接下来的异常捕获部分可能是大家都会忽略的。根据MSDN的文档,InitializeCriticalSection并不保证绝对能创建成功。当内存不足时,就会抛出STATUS_NO_MEMORY异常。所以我们必须捕获这个异常,并进行相应的处理。
  
  话又说回来,在我们平时直接调用临界区的方式中,如果出现这个异常是很麻烦的事情。因为CRITICAL_SECTION对象的内部细节没有公布,我们无法根据CRITICAL_SECTION对象来确定临界区是否有效,也就无法保证代码的健壮性。
  
  如果一切顺利,我们就通过AddTable将临界区放入存储空间中。
  
  AddTable的实现如下:
   
    
  ms_pmpCriticalSection是一个成员变量指针,其在头文件声明如下:
  
  
  
  CriticalSectionData是我们定义的一个结构体,有两个成员变量,一个是指向临界区对象,另一个则记录了进入该临界区的次数:  
    
  
  看到这里,可能有的朋友说,CRITICAL_SECTION不是有个LockCount成员么,直接使用它不就行了?从理论上来说,这是可以的。但对于该结构,微软并没有描述文档,换而言之,微软保留了变更其成员的权利。虽然我们可以相信,微软一般不会做这种费力不讨好的事,但谁知道哪天微软又心血来潮呢?所以,与其将代码建立于微软的信任,还不如建立于我们的掌握之中。
  
  我们稍微回头看看,为什么我们定义ms_pmpCriticalSection为指针,而不是直接作为普通变量?也就是说,为什么不直接这样定义:
   
       
  如果这样定义,会有很大的风险。因为如果有一个类在构造函数调用了CLock::Create,而该类之后的对象又有一个静态声明,那么就会涉及到静态变量初始化顺序的不可控问题。因为在你调用CLock::Create的时候,ms_mpCriticalSection对象很有可能还有进行初始化。
  
  例如:
         
    
       


  为了避免静态变量初始化导致的问题,我们只能采用new来进行分配。
  
  在这里还有一个小技巧,为了避免繁琐的算法,我们直接采用了STL的map。这样,在后续的查找中,我们就能比较轻松。
  
  既然返回的只是序号,那么我们的构造函数也必然需要进行相应的修改:
   
    
  从使用角度来说,我们只需要一个序号的形参即可。但我们想知道能不能成功进入临界区,而构造函数又不能拥有返回值,所以我们就额外多增加了一个pRes形参,用来在函数返回后指明当前进入临界区的状况。
  
  GetObject是我们定义的一个函数,具体实现将在后面说明。现在只需要知道其实返回创建的一个临界区数据。如果获取一个临界区数据对象不为NULL,那么我们在进入临界区之前,先将lLockCount的计数增加1,标明当前进入临界区的次数。
  
  其实很简单,和之前我们的最简单的构造函数相比,只是传入的形参不同。而恰恰只是这一点不同,就能使我们的代码临界区有一个质的飞跃。
  
  再转回头,我们看看GetObject的实现:
    


  代码并不复杂,只是从ms_pmpCriticalSection搜索出对应序号的数值,然后返回。如果该序号不存在,则返回NULL。
  
  到这里,使用上的功能我们基本上已经完成。但似乎还缺了点什么,对,没错,还缺删除函数。删除函数并不复杂,删除临界区后,然后调用delete删除创建的栈,最后再从ms_pmpCriticalSection中删掉相应的序号即可。但我们需要考虑到使用者的便利性,不仅可以删除对应序号的对象,还能简单地删除所有。所以,对于删除函数,我们定义两个。而这两个删除函数,都应该通过不同的手法调用这个内部删除函数:
   
    
  形参是传入一个迭代器,然后根据该迭代器做相应的操作。
  
  那么,我们public的删除相应序号的函数可以如下:  
    
  函数过程简单明了,从通过find函数查找相应key的位置,然后传递给Delete函数。如果Delete返回值为TRUE,那么我们就通过erase擦除。我们之所以没有在Delete(const std::map<DWORD,CriticalSectionData>::iterator &iter)函数中进行擦除,是因为map的迭代器,只要一删除,就会失效,这对后续的操作有所不利。所以,我们只能根据其返回值再确定是否删除。
  
  CheckAndDeleteCriticalSection()函数用来判断ms_pmpCriticalSection是否为空,如果为空,直接调用delete删除该指针:  
    
  
  有了这些基础,那么我们的删除所有的函数就简单了:
   
    
  首先我们判断ms_pmpCriticalSection是否为NULL,为NULL则什么都不做,返回。然后,再遍历整个存储空间,传入其相应的迭代器进行操作。如果Delete返回不为FALSE,我们就调用erase进行删除。
  
  在这里一个小细节需要留意,就是这段代码:
   
    
  对于这段代码,我们不能这么改写:
   
    
  因为我们知道,map容器只要进行erase操作,那么相应的迭代器就会失效。如果采用的是后面一种写法,那么我们调用erase操作后,iter就已经无效,循环中的iter就变成一个不可预料的操作。
  
  现在,就是完整罗列代码的时候了:
  
  头文件:
  
  
  .cpp实现文件:
  


  简单地调用示范如下:
  

posted @ 2009-12-01 11:00  我的一天  阅读(234)  评论(0编辑  收藏  举报