//========================================================================
//TITLE:
// 如何写优雅的代码(5)--远离临界区噩梦
//AUTHOR:
// norains
//DATE:
// Tuesday 01- December-2009
//Environment:
// WINDOWS CE 5.0
//========================================================================
在平时的多线程编程中,我们往往离不开临界区。会用,甚至是用得好的朋友,估计不少;但用得万无一失的,相对而言,可能就会少很多。
不信?我们来看看下面这段代码:
- try
- {
- EnterCriticalSection(&g_cs);
- Cal3rdPartCode(pParam);
- LeaveCriticalSection(&g_cs);
- }
- catch(...)
- {
- //There is error
- //dwRet = 0;
- }
这段代码看起来似乎没有什么问题?但如果说,这段代码会引发孤立临界区的问题,你会信么?换句话说,如果Cal3rdPartCode(pParam)函数会抛出异常,那么代码就直接跳转到catch部分。那事情就严重了,因为代码根本就没有调用LeaveCriticalSection(&g_cs),将g_cs这临界区完全孤立了起来,别的线程就只能一直等待该临界区。
似乎情况很糟糕,不是么?当然,糟糕并不代表陷入世界末日,虽然路途上充满了荆棘,但毕竟我们还是能看到阳光。
我们总不能在一棵树上吊死吧。换个角度,临界区的使用,必须是EnterCriticalSection和LeaveCriticalSection配套使用。那么我们想想,C++里面,有什么东西是强制配套的,根本不用使用者干预的?也许有的朋友已经想到了。没错,是类,类的构造函数和析构函数。
正常使用的话,无论使用者愿不愿意,类的构造函数和析构函数都是被编译器强制配套使用。所以,我们可以在构造函数中调用EnterCriticalSection,在析构函数则是LeaveCriticalSection。
根据此思想,我们最简单的用类来调用临界区的代码诞生了:
- class CLock
- {
- public:
- CLock(LPCRITICAL_SECTION pCriticalSection):
- m_ pCriticalSection(pCriticalSection)
- {
- EnterCriticalSection(m_ pCriticalSection);
- };
- virtual ~CLock()
- {
- LeaveCriticalSection (m_ pCriticalSection);
- };
- private:
- LPCRITICAL_SECTION m_ pCriticalSection;
- };
使用上也是简单明了:
- void CallFunctionProc()
- {
- CLock lock(&g_cs);
- }
但这样完美了么?很遗憾,这只是万里长征的第一步,我们的路还很漫长。最根本的问题,我们如何确保传入的临界区指针是有效的?换句话说,我们无法保证传入的临界区指针是没有调用过DeleteCriticalSection的。
如果代码是如下的书写,很明显,肯定会有我们无法预料的异常:
- void CallFunctionProc()
- {
- DeleteCriticalSection(&g_cs);
- ...
- CLock lock(&g_cs);
- }
那有没有这么一种机制,我们可以通过它进入临界区,但我们却不能让它删除临界区,或是即使删除了,我们也有足够的信息获知。
万事无绝对,既然我们能想到,那么我们就能做到。人有多大胆,地有多大产,在这个奇迹倍出的IT界,有时候也并不只是一句华而不实的口号。
首先,我们将创建临界区的操作封装于类中,使用者只能获得创建的序号;然后,如果想进入临界区,只要传入序号即可;最后,如果不需要该临界区,那么传入序号删除即可。这样的好处是,通过序号这个桥梁,既能使用临界区的功能,又不会引发临界区的不稳定。
我们先来看看创建的函数:
- static DWORD CLock::Create()
- {
- CRITICAL_SECTION* pcsCriticalSection = new CRITICAL_SECTION();
- if(pcsCriticalSection == NULL)
- {
- return Lock::INVALID_INDEX;
- }
- __try
- {
- InitializeCriticalSection(pcsCriticalSection);
- }
- __except(GetExceptionCode() == STATUS_NO_MEMORY)
- {
- //Failed to intialize the critical section, so delete the object created by new operate.
- delete pcsCriticalSection;
- return Lock::INVALID_INDEX;
- }
- return AddTable(pcsCriticalSection);
- }
虽然我们将Create函数封装于CLock类中,但我们不打算需要通过对象才能调用,所以我们以static进行修饰,让其成为类函数。那么,我们调用的时候,就可以直接通过类作用域使用:
- DWORD dwIndex = CLock::Create();
我们留意一下这句:
- CRITICAL_SECTION* pcsCriticalSection = new CRITICAL_SECTION();
这句代码用来在栈中创建CRITICAL_SECTION对象。之所以这么做,是因为如果使用局部变量,那么在函数返回时,局部变量就被删除。而通过new创建出来的对象,除非我们调用delete,否则在程序运行期中一直存在。这样就能达到创建的目的。
接下来的异常捕获部分可能是大家都会忽略的。根据MSDN的文档,InitializeCriticalSection并不保证绝对能创建成功。当内存不足时,就会抛出STATUS_NO_MEMORY异常。所以我们必须捕获这个异常,并进行相应的处理。
话又说回来,在我们平时直接调用临界区的方式中,如果出现这个异常是很麻烦的事情。因为CRITICAL_SECTION对象的内部细节没有公布,我们无法根据CRITICAL_SECTION对象来确定临界区是否有效,也就无法保证代码的健壮性。
如果一切顺利,我们就通过AddTable将临界区放入存储空间中。
AddTable的实现如下:
- static DWORD CLock::AddTable(CRITICAL_SECTION* pcsCriticalSection)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ms_pmpCriticalSection = new std::map<DWORD,CriticalSectionData>();
- if(ms_pmpCriticalSection == NULL)
- {
- return Lock::INVALID_INDEX;
- }
- else
- {
- ms_dwIndex = 0;
- }
- }
- CriticalSectionData newCriticalSectionData = {pcsCriticalSection,0};
- InterlockedIncrement(reinterpret_cast<LONG *>(&ms_dwIndex));
- ms_pmpCriticalSection->insert(std::make_pair(ms_dwIndex,newCriticalSectionData));
- return ms_dwIndex;
- }
ms_pmpCriticalSection是一个成员变量指针,其在头文件声明如下:
- static std::map<DWORD,CriticalSectionData> *ms_pmpCriticalSection;
CriticalSectionData是我们定义的一个结构体,有两个成员变量,一个是指向临界区对象,另一个则记录了进入该临界区的次数:
- struct CriticalSectionData
- {
- CRITICAL_SECTION *pCriticalSection;
- LONG lLockCount;
- };
看到这里,可能有的朋友说,CRITICAL_SECTION不是有个LockCount成员么,直接使用它不就行了?从理论上来说,这是可以的。但对于该结构,微软并没有描述文档,换而言之,微软保留了变更其成员的权利。虽然我们可以相信,微软一般不会做这种费力不讨好的事,但谁知道哪天微软又心血来潮呢?所以,与其将代码建立于微软的信任,还不如建立于我们的掌握之中。
我们稍微回头看看,为什么我们定义ms_pmpCriticalSection为指针,而不是直接作为普通变量?也就是说,为什么不直接这样定义:
- static std::map<DWORD,CriticalSectionData> ms_mpCriticalSection;
如果这样定义,会有很大的风险。因为如果有一个类在构造函数调用了CLock::Create,而该类之后的对象又有一个静态声明,那么就会涉及到静态变量初始化顺序的不可控问题。因为在你调用CLock::Create的时候,ms_mpCriticalSection对象很有可能还有进行初始化。
例如:
- class CTest
- {
- public:
- CText():
- m_dwLock(CLock::Create())
- {
- }
- private:
- DWORD m_dwLock;
- };
- //这里很可能会出错,因为其构造函数调用了CLock::Create,而ms_mpCriticalSection很可能还没有进行初始化
- static CTest test;
为了避免静态变量初始化导致的问题,我们只能采用new来进行分配。
在这里还有一个小技巧,为了避免繁琐的算法,我们直接采用了STL的map。这样,在后续的查找中,我们就能比较轻松。
既然返回的只是序号,那么我们的构造函数也必然需要进行相应的修改:
- CLock::CLock(DWORD dwIndex,BOOL *pRes = NULL):
- m_pcsCriticalSection(NULL)
- {
- //In order to make the source code simple, set the result as FALSE at first.
- if(pRes != NULL)
- {
- *pRes = FALSE;
- }
- m_pcsCriticalSection = GetObject(dwIndex);
- if(m_pcsCriticalSection != NULL)
- {
- if(pRes != NULL)
- {
- *pRes = TRUE;
- }
- InterlockedIncrement(&m_pcsCriticalSection->lLockCount);
- EnterCriticalSection(m_pcsCriticalSection->pCriticalSection);
- }
- }
从使用角度来说,我们只需要一个序号的形参即可。但我们想知道能不能成功进入临界区,而构造函数又不能拥有返回值,所以我们就额外多增加了一个pRes形参,用来在函数返回后指明当前进入临界区的状况。
GetObject是我们定义的一个函数,具体实现将在后面说明。现在只需要知道其实返回创建的一个临界区数据。如果获取一个临界区数据对象不为NULL,那么我们在进入临界区之前,先将lLockCount的计数增加1,标明当前进入临界区的次数。
其实很简单,和之前我们的最简单的构造函数相比,只是传入的形参不同。而恰恰只是这一点不同,就能使我们的代码临界区有一个质的飞跃。
再转回头,我们看看GetObject的实现:
- CLock::CriticalSectionData* CLock::GetObject(DWORD dwIndex)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return NULL;
- }
- std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->find(dwIndex);
- if(iter != ms_pmpCriticalSection->end())
- {
- return &iter->second;
- }
- else
- {
- return NULL;
- }
- }
代码并不复杂,只是从ms_pmpCriticalSection搜索出对应序号的数值,然后返回。如果该序号不存在,则返回NULL。
到这里,使用上的功能我们基本上已经完成。但似乎还缺了点什么,对,没错,还缺删除函数。删除函数并不复杂,删除临界区后,然后调用delete删除创建的栈,最后再从ms_pmpCriticalSection中删掉相应的序号即可。但我们需要考虑到使用者的便利性,不仅可以删除对应序号的对象,还能简单地删除所有。所以,对于删除函数,我们定义两个。而这两个删除函数,都应该通过不同的手法调用这个内部删除函数:
- static BOOL CLock::Delete(const std::map<DWORD,CriticalSectionData>::iterator &iter)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- if(iter == ms_pmpCriticalSection->end())
- {
- return FALSE;
- }
- if(GetLockCount(iter->first) != 0)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- if(iter->second.pCriticalSection == NULL)
- {
- return FALSE;
- }
- DeleteCriticalSection(iter->second.pCriticalSection);
- delete iter->second.pCriticalSection;
- return TRUE;
- }
形参是传入一个迭代器,然后根据该迭代器做相应的操作。
那么,我们public的删除相应序号的函数可以如下:
- BOOL CLock::Delete(DWORD dwIndex)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->find(dwIndex);
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter);
- CheckAndDeleteCriticalSection();
- return TRUE;
- }
- else
- {
- return FALSE;
- }
- }
函数过程简单明了,从通过find函数查找相应key的位置,然后传递给Delete函数。如果Delete返回值为TRUE,那么我们就通过erase擦除。我们之所以没有在Delete(const std::map<DWORD,CriticalSectionData>::iterator &iter)函数中进行擦除,是因为map的迭代器,只要一删除,就会失效,这对后续的操作有所不利。所以,我们只能根据其返回值再确定是否删除。
CheckAndDeleteCriticalSection()函数用来判断ms_pmpCriticalSection是否为空,如果为空,直接调用delete删除该指针:
- static void CLock::CheckAndDeleteCriticalSection()
- {
- if(ms_pmpCriticalSection->empty() != FALSE)
- {
- delete ms_pmpCriticalSection;
- ms_pmpCriticalSection = NULL;
- }
- }
有了这些基础,那么我们的删除所有的函数就简单了:
- static void CLock::DeleteAll()
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return;
- }
- for(std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->begin(); iter != ms_pmpCriticalSection->end(); )
- {
- //The iterator would be destroy when you call erase,so I must call just as follows.
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter ++);
- }
- else
- {
- ++ iter;
- }
- }
- CheckAndDeleteCriticalSection();
- }
首先我们判断ms_pmpCriticalSection是否为NULL,为NULL则什么都不做,返回。然后,再遍历整个存储空间,传入其相应的迭代器进行操作。如果Delete返回不为FALSE,我们就调用erase进行删除。
在这里一个小细节需要留意,就是这段代码:
- for(std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->begin(); iter != ms_pmpCriticalSection->end(); )
- {
- //The iterator would be destroy when you call erase,so I must call just as follows.
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter ++);
- }
- else
- {
- ++ iter;
- }
- }
对于这段代码,我们不能这么改写:
- for(std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->begin(); iter != ms_pmpCriticalSection->end();++ iter )
- {
- //The iterator would be destroy when you call erase,so I must call just as follows.
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter);
- }
- }
因为我们知道,map容器只要进行erase操作,那么相应的迭代器就会失效。如果采用的是后面一种写法,那么我们调用erase操作后,iter就已经无效,循环中的iter就变成一个不可预料的操作。
现在,就是完整罗列代码的时候了:
头文件:
- #pragma once
- #include "Windows.h"
- #include <map>
- namespace Lock
- {
- const DWORD INVALID_INDEX = 0xFFFFFFFF;
- };
- class CLock
- {
- public:
- //----------------------------------------------------------------------------------------
- //Description:
- // Create the lock object and return the index
- //Return Values:
- // If the value is INVALID_INDEX, it means failed. Others is succeeded.
- //-----------------------------------------------------------------------------------------
- static DWORD Create();
- //---------------------------------------------------------------------------------------------
- //Description:
- // Delete all the created critical section.When you call the function,you must be sure that
- //there isn't other threads to enter the critical section.After succeed in calling the function,
- //the GetSize() returns 0, otherwise it doesn't delete all the critical section object.
- //---------------------------------------------------------------------------------------------
- static void DeleteAll();
- //---------------------------------------------------------------------------------------------
- //Description:
- // Delete the created critical section on the index.
- //Parameters:
- // dwIndex : [in]The index of object to delete
- //Return Values:
- // FALSE means failed, maybe the index is locked.
- //---------------------------------------------------------------------------------------------
- static BOOL Delete(DWORD dwIndex);
- //----------------------------------------------------------------------------------------
- //Description:
- // Get the size of the the critical section
- //----------------------------------------------------------------------------------------
- static DWORD GetSize();
- //------------------------------------------------------------------------------------------------
- //Description:
- // Get the lock count
- //Parameters:
- // dwIndex : [in] The index to check.
- //Return Values:
- // 0 means there is not the thread to lock the index of object or the index if invalid
- //Others means the count of lock
- //---------------------------------------------------------------------------------------------------
- static LONG GetLockCount(DWORD dwIndex);
- //----------------------------------------------------------------------------------------
- //Description:
- // The construct is used for enter the critical section
- //Parameters:
- // dwIndex : [in] The index to lock
- // pRes : [in] The result of locking. TRUE means succeeded, FALSE means failed.
- // If you don't want this value,you could set NULL.
- //----------------------------------------------------------------------------------------
- CLock(DWORD dwIndex,BOOL *pRes = NULL);
- //----------------------------------------------------------------------------------------
- //Description:
- // The destruct is used for leave the critical section
- //----------------------------------------------------------------------------------------
- virtual ~CLock();
- private:
- //----------------------------------------------------------------------------------------
- //Description:
- // Add the critical section object to the table.
- //Parameters:
- // pcsCriticalSection : [in] The object to add.
- //Parameters:
- // Return the index in the table.
- //--------------------------------------------------------------------------------------
- static DWORD AddTable(CRITICAL_SECTION* pcsCriticalSection);
- //----------------------------------------------------------------------------------------
- //Description:
- // The struct value is for the internal value
- //----------------------------------------------------------------------------------------
- struct CriticalSectionData
- {
- CRITICAL_SECTION *pCriticalSection;
- LONG lLockCount;
- };
- //----------------------------------------------------------------------------------------
- //Description:
- // Delete the object base on the iterator positon
- //Parameters:
- // iter : [in] The iterator to delete
- //-----------------------------------------------------------------------------------------
- static BOOL Delete(const std::map<DWORD,CriticalSectionData>::iterator &iter);
- //----------------------------------------------------------------------------------------
- //Description:
- // Get the critical section object
- //Parameters:
- // dwIndex : [in] The index to get.
- //Return Values:
- // NULL means failed,others is succeeded.
- //----------------------------------------------------------------------------------------
- static CriticalSectionData* GetObject(DWORD dwIndex);
- //----------------------------------------------------------------------------------------
- //Description:
- // Check and delete the critical section which is allocate by new operate.
- //----------------------------------------------------------------------------------------
- static void CheckAndDeleteCriticalSection();
- private:
- static std::map<DWORD,CriticalSectionData> *ms_pmpCriticalSection;
- static DWORD ms_dwIndex;
- CriticalSectionData *m_pcsCriticalSection;
- };
.cpp实现文件:
- #include "Lock.h"
- //----------------------------------------------------------------------------------
- //The static member
- std::map<DWORD,CLock::CriticalSectionData> * CLock::ms_pmpCriticalSection = NULL;
- DWORD CLock::ms_dwIndex = 0;
- //----------------------------------------------------------------------------------
- CLock::CLock(DWORD dwIndex,BOOL *pRes):
- m_pcsCriticalSection(NULL)
- {
- //In order to make the source code simple, set the result as FALSE at first.
- if(pRes != NULL)
- {
- *pRes = FALSE;
- }
- m_pcsCriticalSection = GetObject(dwIndex);
- if(m_pcsCriticalSection != NULL)
- {
- if(pRes != NULL)
- {
- *pRes = TRUE;
- }
- InterlockedIncrement(&m_pcsCriticalSection->lLockCount);
- EnterCriticalSection(m_pcsCriticalSection->pCriticalSection);
- }
- }
- CLock::~CLock()
- {
- if(m_pcsCriticalSection != NULL)
- {
- LeaveCriticalSection(m_pcsCriticalSection->pCriticalSection);
- InterlockedDecrement(&m_pcsCriticalSection->lLockCount);
- }
- }
- DWORD CLock::Create()
- {
- CRITICAL_SECTION* pcsCriticalSection = new CRITICAL_SECTION();
- if(pcsCriticalSection == NULL)
- {
- return Lock::INVALID_INDEX;
- }
- __try
- {
- InitializeCriticalSection(pcsCriticalSection);
- }
- __except(GetExceptionCode() == STATUS_NO_MEMORY)
- {
- //Failed to intialize the critical section, so delete the object created by new operate.
- delete pcsCriticalSection;
- return Lock::INVALID_INDEX;
- }
- return AddTable(pcsCriticalSection);
- }
- void CLock::DeleteAll()
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return;
- }
- for(std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->begin(); iter != ms_pmpCriticalSection->end(); )
- {
- //The iterator would be destroy when you call erase,so I must call just as follows.
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter ++);
- }
- else
- {
- ++ iter;
- }
- }
- CheckAndDeleteCriticalSection();
- }
- CLock::CriticalSectionData* CLock::GetObject(DWORD dwIndex)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return NULL;
- }
- std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->find(dwIndex);
- if(iter != ms_pmpCriticalSection->end())
- {
- return &iter->second;
- }
- else
- {
- return NULL;
- }
- }
- DWORD CLock::GetSize()
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return 0;
- }
- return ms_pmpCriticalSection->size();
- }
- DWORD CLock::AddTable(CRITICAL_SECTION* pcsCriticalSection)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ms_pmpCriticalSection = new std::map<DWORD,CriticalSectionData>();
- if(ms_pmpCriticalSection == NULL)
- {
- return Lock::INVALID_INDEX;
- }
- else
- {
- ms_dwIndex = 0;
- }
- }
- CriticalSectionData newCriticalSectionData = {pcsCriticalSection,0};
- InterlockedIncrement(reinterpret_cast<LONG *>(&ms_dwIndex));
- ms_pmpCriticalSection->insert(std::make_pair(ms_dwIndex,newCriticalSectionData));
- return ms_dwIndex;
- }
- BOOL CLock::Delete(DWORD dwIndex)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- std::map<DWORD,CriticalSectionData>::iterator iter = ms_pmpCriticalSection->find(dwIndex);
- if(Delete(iter) != FALSE)
- {
- ms_pmpCriticalSection->erase(iter);
- CheckAndDeleteCriticalSection();
- return TRUE;
- }
- else
- {
- return FALSE;
- }
- }
- BOOL CLock::Delete(const std::map<DWORD,CriticalSectionData>::iterator &iter)
- {
- if(ms_pmpCriticalSection == NULL)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- if(iter == ms_pmpCriticalSection->end())
- {
- return FALSE;
- }
- if(GetLockCount(iter->first) != 0)
- {
- ASSERT(FALSE);
- return FALSE;
- }
- if(iter->second.pCriticalSection == NULL)
- {
- return FALSE;
- }
- DeleteCriticalSection(iter->second.pCriticalSection);
- delete iter->second.pCriticalSection;
- return TRUE;
- }
- LONG CLock::GetLockCount(DWORD dwIndex)
- {
- CriticalSectionData *pCriticalSection = GetObject(dwIndex);
- if(pCriticalSection == NULL)
- {
- return 0;
- }
- else
- {
- return pCriticalSection->lLockCount;
- }
- }
- void CLock::CheckAndDeleteCriticalSection()
- {
- if(ms_pmpCriticalSection->empty() != FALSE)
- {
- delete ms_pmpCriticalSection;
- ms_pmpCriticalSection = NULL;
- }
- }
简单地调用示范如下:
- //获取相应的序号
- const DWORD dwIndex = CLock::Create();
- //进入临界区
- CLock lock(dwIndex);
- //删除相应的临界区
- CLock::Delete(dwIndex);