C++析构函数妙用-- 不会忘记的Unlock
在C++中,类的析构函数被解释为用于销毁 对象的代码块,在对象将被从内存中清除之前调用。而事实上,利用析构函数的调用时机,可以做很多普通的过程控制代码很难做到的 事情。
比如在多线程程序中的锁。在加锁和开锁的过程中,必须非常小心地配对,稍有不慎就 会少了开锁次数,使资源锁得不到打开。在有复杂控制的程序体中,这种维护是很烦琐的。
以Windows程序为例,在类 CWithLock中使用临界区定义一个锁,成员函数fun将被多个线程调 用。
CWithLock
{
public:
CWithLock();
~CWithLock();
CRITICAL_SECTION m_csAccessLock; // 作为锁的临界区
void fun(); // 将被多线程调用的成员函数
};
简单的情况下,在fun函数的定义中我们可 以简单地使用锁,如下:
void fun()
{
EnterCriticalSection(&m_csAccessLock);
// 函数体
// 。。 。。
LeaveCriticalSection(&m_csAccessLock);
}
然而,当函数体控制很复杂的时候,我们不得不在每个return语句前加上开锁语句,如下:
void fun()
{
EnterCriticalSection(&m_csAccessLock);
// 函数体
if(..)
{
// 。。。。
LeaveCriticalSection (&m_csAccessLock);
return;
}else
{
try
{
if (…)
{
// 。。。。
LeaveCriticalSection(&m_csAccessLock);
return;
}
}catch(…)
{
// 。。。。
LeaveCriticalSection (&m_csAccessLock);
return;
}
}
// 。。 。。
LeaveCriticalSection(&m_csAccessLock);
}
可以看出,随着程序控制的复杂化,不得不在多个地方增加开锁代码。这种多处都不得 不执行的相同操作增加了代码失误的可能性,以日后程序的维护和修改过程中,也会造成很多麻烦。如果在fun的执行过程中要使用多个共享资源,情况将更加糟糕。当代码失误存在于一个很少执行到的代码分支里 时,无疑是给程序埋下了一个定时炸弹。
加锁和开锁的函数调用代码看起来也很像,在我的程序生涯中还遇到喜欢“复制 -粘贴”的同事把这两个函数写错的事情。发现这个错误所用的代价可是很大的。
有没有办法可以放松一下代码人员紧张的神经呢?答案是有,而且不止一种。
第一种,利用被受垢病的goto语句。把所有的开锁 工作放到fun函数的尾部,把所有的return语句换成 goto语句,跳到开锁的位置。如下:
void fun()
{
EnterCriticalSection(&m_csAccessLock);
// 函数体
if(..)
{
// 。。。。
goto end;
}else
{
try
{
if (…)
{
// 。。。。
goto end;
}
}catch(…)
{
// 。。。。
goto end;
}
}
// 。。 。。
end:
LeaveCriticalSection(&m_csAccessLock);
}
看起来好些了,但实际操作过程中,goto的使用给 代码也会带来很多麻烦。如goto不能跳过变量的声明;无法在函数休内动态地对某一资源开锁,以减少上 锁时间;所有的变量都声明的第一个goto之前,代码看上去也很丑,至少可读性会受到响。
第二种方法,使用try-catch异常处理机制中的 __finally代码块。把函数体作为try代码块的一部分,在这里可以随意调用 return返回。把开锁语句放进__finally代码块中,可以保证最后总 会调用到开锁语句。
这种方法同样问题多多,不只是难看,而且也同样不够灵活。
最好的方法当然是第三种,也就是利用析构函数来解锁。在详述这种方法前,我们先看 看函数体或程序块的执行过程。
一个程序块就是大括号“{}”内的整个代码块。一 个函数体也是一个程序块。程序块内声明的变量,在声明时执行该变量的构造函数;当控制离开程序块时,无论执行到 “}”,还是使用break或return语句 跳出,每个在程序块里声明的变量都将被析构,他们的析构函数都将被调用。
利用这个特点,如果我们使用一个对象的实例来加锁,并在这个对象的析构函数中解锁 ,那么我们将无需显式地声明解锁过程,并总能保证加锁状态只在程序块里有效。
我们设计一个简单的类来实现这个功能:
CLock
{
public:
CLock(CRITICAL_SECTION *p_door) // 在构造函数中加锁
{
if (NULL != (m_pDoor = p_door))
EnterCriticalSection(m_pDoor);
}
~CLock() // 在析构函数中开锁
{
if (NULL != m_pDoor))
LeaveCriticalSection (m_pDoor);
}
privated:
CRITICAL_SECTION * m_pDoor;
};
使用CLock类之后,我们将fun的函数体改写一下:
void fun()
{
CLock Lock(&m_csAccessLock); // 使用CLock的构造函数加锁
// 函数体
if(..)
{
// 。。。。
return; // 无需再开锁
}else
{
try
{
if (…)
{
// 。。。。
return; // 无需再开锁
}
}catch(…)
{
// 。。。。
return; // 无需再开锁
}
}
// 。。 。。// 无需再开锁
}
我们可以看到,除了程序的第一行以外,其余地方均和普通的单线程程序没有什么不 同。程序员不用在担心开锁的问题,可以说有了一个永远不会忘记的Unlock。
不仅如此,我们还可以使用“{}”来界定 锁的有效范围,可以在任何地方随时锁定其它的资源,并在使用结束的时候,自动开锁。如果要同时在一个代码块里锁定多个资源,析 构函数还能保证开锁的顺序,在一定程度上避免死锁。
上述CLock代码只是一个例子,并不是最好的设计 ,并且也不能满足所有的算法要求。在有些特殊的算法中,就要求资源在一个程序块中加锁,而在另一个程序块中解锁。另外还要增加 TryLock功能,使线程在访问被锁的资源后,仍能运行。
在考虑了所有的需求之后,可以设计出更好的类来实现锁。下面给出一个笔者使用的 类,仅供参考。
//******** CWithLock.h ***********************
被共享实例的类
class CLock;
class CWithLock
{
public:
CWithLock ();
~CWithLock ();
HANDLE GetOwnerThread();
protected:
CRITICAL_SECTION m_csAccess;
inline void Lock();
inline void UnLock();
inline BOOL TryLock();
friend class CLock;
};
//******** CWithLock.cpp ***********************
CWithLock::CWithLock()
{
InitializeCriticalSection( &m_csAccess );
}
CWithLock::~CWithLock()
{
DeleteCriticalSection( &m_csAccess );
}
void CWithLock::Lock()
{
EnterCriticalSection (&m_csAccess);
}
LeaveCriticalSection (&m_csAccess);
}
BOOL CWithLock::TryLock()
{
return TryEnterCriticalSection(&m_csAccess);
}
HANDLE CWithLock::GetOwnerThread()
{
return m_csAccess.OwningThread;
}
//******** CLock.h ***********************
class CWithLock;
// 用来锁 CWithLock实例的类
class CLock
{
public:
CLock( CWithLock &door, bool bLook = true);
~CLock ();
void Lock();
void UnLock();
BOOL TryLock ();
static void Lock(CWithLock &door);
static void UnLock(CWithLock &door);
private:
bool m_bLocked;
CWithLock * m_pDoor;
};
//******** CLock.cpp ***********************
CLock::CLock( CWithLock &door, bool b_lock)
{
m_pDoor = &door;
m_bLocked = b_lock;
if (m_bLocked)
Lock (*m_pDoor);
}
CLock::~CLock()
{
if (m_bLocked)
UnLock (*m_pDoor);
}
void CLock::Lock()
{
if(!m_bLocked)
{
m_bLocked = true;
Lock (*m_pDoor);
}
}
if (m_bLocked)
{
m_bLocked = false;
UnLock (*m_pDoor);
}
}
BOOL CLock::TryLock()
{
if(!m_bLocked)
{
if ( m_pDoor->TryLock() )
{
m_bLocked = true;
return TRUE;
}
return FALSE;
}
return TRUE;
}
door.Lock();
}
door.UnLock();
}