如何创建一个简单的C++同步锁框架(译)

翻译自codeproject上面的一篇文章,题目是:如何创建一个简单的c++同步锁框架

目录

  1. 介绍

  2. 背景

  3. 临界区 & 互斥 & 信号

    1. 临界区

    2. 互斥

    3. 信号

    4. 更多信息

  4. 建立锁框架的目的

  5. BaseLock

  6. 临界区类

    1. 构造/拷贝构造

    2. 析构

    3. Lock/TryLock

    4. TryLockFor

    5. 解锁

  7. 信号量类

    1. 构造/拷贝构造

    2. 析构

    3. Lock/TryLock/TryLockFor

    4. 解锁

    5. 释放

  8. 互斥类

    1. 构造/拷贝构造

    2. 析构

    3. Lock/ TryLock/TryLockFor

    4. 解锁

  9. 无锁类

    1. 实现

  10. 自动释放锁类

    1. 实现

  11. 例子

    1. 声明同步对象

    2. 声明带初始计数的信号量对象

    3. 声明跨进程的互斥/信号量对象

    4. 同步示例1 (手动加锁)

    5. 同步示例2 (自动加锁)

    6. 同步示例3 (TryLock/TryLockFor)

  12. 更多的示例

  13. 结论

  14. 参考

 

1 介绍

  开发多线程/多进程应用时, 同步是一个很大的问题. 你需要让你的程序同时满足单线程和多线程/多进程的应用场合,当然如何管理同步又是另一个话题了。利用锁(同步)机制不会自动帮组你解决上诉问题,但是有助于你找到解决问题的方法。这篇文章向你解释了如何构建简单锁框架,如何在你的应用中运用它。可能很多人已经开发了自己的锁框架,或者利用成熟的库比如 boost library或者 Intel TBB。但是有时候它们对于你的应用来说可能过于臃肿,也可能并不是你想要的。通过阅读本文,你可能构建符合你自己的锁框架。

本文将解释下面这些东西:

  • BaseLock

  • Critical Section

  • Semaphore

  • Mutex

  • NoLock

  • Auto-release Lock

  • 示例

  提示: 本文既不是要讲解高级的锁框架,也不是要讲解同步基元的区别。正如题目所说,本文只会告诉你:如何构建一个简单的锁框架,你可以根据本文的框架结合你自己的需要,运用或者修改它


2 背景

这里你必须了解以下几个同步基元的概念和他们之间的区别:

  • Critical Section

  • Semaphore

  • Mutex

如果你对MFC中的同步类(CSingleLock/CMultiLock, CCriticalSection, CSemaphore, and CMutex)有所了解,这将有助于你理解并运用这些类来代替windows函数库去实现一个锁框架。

 

3 Critical Section & Mutex & Semaphore

下面这段主要来自于这篇文章 "Synchronization in Multithreaded Applications with MFC" ,由Arman S所写,我只是引用其中的内容,所有权为他本人所有。

  • Critical Section

临界区工作于用户模式,如果需要可以进入内核模式。如果一个线程运行一段位于临界区的代码,它首先会产生一个自旋区并在一段时间的等待之后进入内核模式并等待临界区对象。实际上临界区由自旋计数和信号量组成。前者用于用户态模式下的等待,后者用于内核模式下的等待(睡眠)。在win32 API中,结构体CRITICAL_SECTION表示了一个临界区。在MFC中,用类CCriticalSection表示。概念上一个临界区表示一段可运行的代码,并保证这段代码在执行过程中不会被中断打断。这样做是为了保证一个单线程在访问共享资源时,拥有绝对占有权。

  • Mutex

互斥与临界区一样,也是用于同步访问时保护共享资源。互斥在内核中实现,因此可以进入内核模式。互斥不仅可以用于线程之间,也可以用于进程之间。一般用于不同进程之间时,它都拥有一个惟一的名字,我们称这种互斥为命名互斥。

  • Semaphore

为了限制访问共享资源的线程数量,我们需要使用信号量。信号量是内核对象,它有一个计数变量用于表示当前正在使用共享资源的线程数。举个例子,下面的代码用MFCCSemaphore类创建了一个信号量,它能保证在设定的时间周期(这个值有第一个参数设定)内能够同时访问共享资源的最大线程数量,并且初始化完成后的时刻没有线程对资源拥有所有权(有第二个参数设定)。

HANDLE g_sem = CreateSemaphore(NULL,5,5,NULL);
  • 更多信息

如果想了解更多,请阅读Arms S的这篇文章 here

4 锁框架的目的

  在windows开发中,最常见的同步基元是Critical Section, Semaphore, Mutex. 对于初学者他们可能经常使用这些同步基元,却很难去了解同步概念本身。因此建立一个简单的锁框架的出发点是使得不同基元拥有统一的接口和调用方式, 但是按照它们原始的机制去工作,这样更容易理解。

5 BaseLock

  虽然不同的同步基元作用不一样,但是仍然有一些基本的功能共同点。

  • Lock

  • TryLock

  • TryLockFor (试加锁一段时间)

  • Unlock

  可能有人会想到更多的共同点,但是你可以根据你的需求选择构建你自己的锁框架,这里我只是用这4个函数来简化我们想要说明的问题。我们的 BaseLock类是一个纯虚类,如下所示:

// BaseLock.h
  
class BaseLock
{ 
public:
   BaseLock();
   virtual ~BaseLock();
   virtual bool Lock() =0;
   virtual long TryLock()=0;
   virtual long TryLockFor(const unsigned int dwMilliSecond)=0; 
   virtual void Unlock()=0; 
};

注意一点:对于互斥而言,Lock函数返回布尔型,对于其它的基元,这个函数总是返回真。

6 Critical Section

  因为CRITICAL_SECTION结构体已经存在了,所以这里我们将这个类命名为CriticalSectionEx。这个类继承自BaseLock类,也是CRITICAL_SECTION对象的一个接口类。与BaseLock类相似,我们定义如下:

// CriticalSectionEx.h
#include "BaseLock.h"

class CriticalSectionEx :public BaseLock
{
public:
   CriticalSectionEx();
   CriticalSectionEx(const CriticalSectionEx& b);
   virtual ~CriticalSectionEx();
   CriticalSectionEx & operator=(const CriticalSectionEx&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
private:
   CRITICAL_SECTION m_criticalSection;
   int m_lockCounter;
}; 
  • 注意, 赋值操作符重载函数"operator="返回自身,是因为我不想让成员对象CRITICAL_SECTION被改变,如果"operator="没有被重载,它会自动调用拷贝函数,这样CRITICAL_SECTION对象的值就会被修改替代。另外为什么没有将赋值操作符函数设为私有?原因在于当一个类比如说A试图从其它类拷贝时,访问私有函数会产生编译错误。

  • 另外,临界区在重入时,如果进入和离开次数不对等将产生未定义行为, 所以我们加入了成员变量m_lockCounter,用来跟踪locks/unlocks的数量。

下面的5个函数是临界区使用时的常用函数:

  • InitializeCriticalSection

  • DeleteCriticalSection

  • EnterCriticalSection

  • LeaveCriticalSection

  • TryEnterCriticalSection

更多信息请参考MSDN。这5个函数将合并如下:

  Constructor/Copy Constructor

// CriticalSectionEx.cpp
CriticalSectionEx::CriticalSectionEx() :BaseLock()
{
   InitializeCriticalSection(&m_criticalSection);
   m_lockCounter=0;
}
CriticalSectionEx::CriticalSectionEx(const CriticalSectionEx& b):BaseLock()
{
   InitializeCriticalSection(&m_criticalSection);
   m_lockCounter=0; 
}

  Destructor

// CriticalSectionEx.cpp
CriticalSectionEx::~CriticalSectionEx()
{
   assert(m_lockCounter==0);
   DeleteCriticalSection(&m_criticalSection);
} 

  如果CRITICAL_SECTION对象被初始化后,它必须要释放,所以将DeleteCriticalSection放在析构函数里,CriticalSectionEx析构时CRITICAL_SECTION将被自动释放。注意如果m_lockCounter不为0,这意味着加锁和解锁次数不相同,这将导致未定义行为。

  Lock/TryLock

// CriticalSectionEx.cpp
bool CriticalSectionEx::Lock()
{
   EnterCriticalSection(&m_criticalSection);
   m_lockCounter++
   return true;
}
long CriticalSectionEx::TryLock()
{
   long ret=TryEnterCriticalSection(&m_criticalSection);
   if(ret)
      m_lockCounter++;
   return ret;
}   

这是非常直观的实现方式。不解释了!

  TryLockFor

// CriticalSectionEx.cpp
long CriticalSectionEx::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0;
   if(ret=TryEnterCriticalSection(&m_criticalSection))
   {
      m_lockCounter++;
      return ret;
   }
   else
   {
      unsigned int startTime,timeUsed;
      unsigned int waitTime=dwMilliSecond;
      startTime=GetTickCount();
      while(WaitForSingleObject(m_criticalSection.LockSemaphore,waitTime)==WAIT_OBJECT_0)
      {
         if(ret=TryEnterCriticalSection(&m_criticalSection))
         {
            m_lockCounter++;
            return ret;
         }
         timeUsed=GetTickCount()-startTime;
         waitTime=waitTime-timeUsed;
         startTime=GetTickCount();
      }
      return 0;
   }
}    

  注意,原始的TryEnterCriticalSection函数没有时间参数(根据MSDN的说法是为了防止死锁)。因此我用一点技巧来模拟一定时间段内的TryLock机制,使用的时候你需要注意一点,因为微软并没有为CRITICAL_SECTION对象实现这个接口。这里并不保证进入临界区的顺序。

  Unlock

// CriticalSectionEx.cpp
void CriticalSectionEx::Unlock()
{
   assert(m_lockCounter>=0);
   LeaveCriticalSection(&m_criticalSection);
}   

  如果获取到CRITICAL_SECTION对象,释放时通过调用"LeaveCriticalSection"来解锁表示离开临界区。主要到m_lockCounter必须大于等于0,否则表示解锁次数大于加锁次数。

7 Semaphore Class

  我们的信号量类也继承自BaseLock类,是Semaphore对象的接口类 (实际上是一个句柄)。如下:

// Semaphore.h
#include "BaseLock.h"
 
class Semaphore :public BaseLock
{
public:
   Semaphore(unsigned int count=1,const TCHAR *semName=_T(""), LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);
   Semaphore(const Semaphore& b);
   virtual ~Semaphore();
   Semaphore & operator=(const Semaphore&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
   long Release(long count, long * retPreviousCount);
private:
   /// Actual Semaphore
   HANDLE m_sem;
   /// Creation Info
   LPSECURITY_ATTRIBUTES m_lpsaAttributes;
   /// Semaphore Flag
   unsigned int m_count;
};  

  注意, 信号量类的构造函数的参数有2个:semNamelpsaAttributes,而BaseLockCriticalSectionEx的构造函数没有。另外它的构造函数调用时可以不加参数,因为有默认参数,这几个参数的意义如下:

    • count 信号量的加锁次数

      • 缺省值是1,代表了二值信号量

    • semName 信号量名称

      • 缺省值是NULL

      • 在不同进程间同步时需要设置这个参数 (参考MSDN.)

    • lpsaAttributes 新的信号量的安全属性

      • 缺省值是NULL,代表了缺省的安全属性

      • (想了解更多,参考MSDN.)

下面的函数是信号量的4个常用操作函数:

  • CreateSemaphore

  • CloseHandle

  • WaitForSingleObject

  • ReleaseSemaphore

更多信息可参考MSDN,这4个函数合并如下:

  Constructor/Copy Constructor

// Semaphore.cpp
 
Semaphore::Semaphore(unsigned int count,const TCHAR *semName, LPSECURITY_ATTRIBUTES lpsaAttributes) :BaseLock()
{ 
   m_lpsaAttributes=lpsaAttributes;
   m_count=count;
   m_sem=CreateSemaphore(lpsaAttributes,count,count,semName);
} 
Semaphore::Semaphore(const Semaphore& b) :BaseLock()
{ 
   m_lpsaAttributes=b.m_lpsaAttributes;
   m_count=b.m_count;
   m_sem=CreateSemaphore(m_lpsaAttributes,m_count,m_count,NULL);
}  

  这里的拷贝构造函数,复制了另一个类的安全属性和计数值,然后创建一个新的信号量对象。

  Destructor

// Semaphore.cpp
 
Semaphore::~Semaphore()
{
   CloseHandle(m_sem);
} 

  Lock/TryLock/TryLockFor

// Semaphore.cpp
bool Semaphore::Lock()
{
   WaitForSingleObject(m_sem,INIFINITE);
   return true;
}
long Semaphore::TryLock()
{
   long ret=0;
   if(WaitForSingleObject(m_sem,0) == WAIT_OBJECT_0 )
      ret=1;
   return ret;
}
long Semaphore::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0;
   if( WaitForSingleObject(m_sem,dwMilliSecond) == WAIT_OBJECT_0)
      ret=1;
   return ret;
}

  Unlock

// Semaphore.cpp
 
void Semaphore::Unlock()
{
   ReleaseSemaphore(m_sem,1,NULL);
} 

  Release

  如Ahmed Charfeddine建议的那样,对于信号量的Unlock函数最好可以接受一个表示释放计数的参数并返回成功或失败结果。这是信号量比较特别的地方,你可以重新写一个Release函数进行扩展。

// Semaphore.cpp
 
void Semaphore::Release(long count, long * retPreviousCount)
{
   return ReleaseSemaphore(m_sem,count,retPreviousCount); 
}
//用法
BaseLock *lock = new Semaphore(10);
...
BOOL ret = dynamic_cast<Semaphore*>(lock)->Release(5);
// OR
Semaphore lock(10);
...
BOOL ret = lock.Release(5);  



8 Mutex Class

  互斥类继承自BaseLock,是互斥对象的一个接口类。

// Mutex.h
#include "BaseLock.h"



class Mutex :public BaseLock
{
public:
   Mutex(const TCHAR *mutexName=NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);
   Mutex(const Mutex& b);
   virtual ~Mutex();
   Mutex & operator=(const Mutex&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
   bool IsMutexAbandoned()
   {
      return m_isMutexAbandoned;
   )
private:
   /// Mutex
   HANDLE m_mutex;
   /// Creation Security Info
   LPSECURITY_ATTRIBUTES m_lpsaAttributes;
   /// Flag for whether this mutex is abandoned or not.
   bool m_isMutexAbandoned;
}; 

  注意, 互斥量可以被舍弃,但是我们不想单独为了互斥量改变统一的接口,所以我增加了一个函数IsMutexAbandoned用来检查此互斥量是否在加锁失败时被舍弃了。

互斥量的常用函数如下:

  • CreateMutex

  • CloseHandle

  • WaitForSingleObject

  • ReleaseMutex

更多信息请参考MSDN.

  Constructor/Copy Constructor

// Mutex.cpp
Mutex::Mutex(const TCHAR *mutexName, LPSECURITY_ATTRIBUTES lpsaAttributes) :BaseLock()
{
   m_isMutexAbandoned=false;
   m_lpsaAttributes=lpsaAttributes;
   m_mutex=CreateMutex(lpsaAttributes,FALSE,mutexName);
}
Mutex::Mutex(const Mutex& b)
{
   m_isMutexAbandoned=false; 
   m_lpsaAttributes=b.m_lpsaAttributes;
   m_mutex=CreateMutex(m_lpsaAttributes,FALSE,NULL);
}  

  Destructor

// Mutex.cpp
 
Mutex::~Mutex()
{
   CloseHandle(m_mutex);
} 

  Lock/TryLock/TryLockFor

// Mutex.cpp
void Mutex::Lock()
{
   bool returnVal = true;
   unsigned long res=WaitForSingleObject(m_mutex,INIFINITE);
   if(res=WAIT_ABANDONED)
   {
      m_isMutexAbandoned=true;
      returnVal=false; 
   }
   return returnVal;
}
long Mutex::TryLock()
{
   long ret=0;
   unsigned long mutexStatus= WaitForSingleObject(m_mutex,0);
   if(mutexStatus== WAIT_OBJECT_0)
      ret=1;
   else if(mutexStatus==WAIT_ABANDONED)
      m_isMutexAbandoned=true;
   return ret;
}
long Mutex::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0; 
   unsigned long mutexStatus= WaitForSingleObject(m_mutex,dwMilliSecond);
   if(mutexStatus==WAIT_OBJECT_0)
      ret=1;
   else if(mutexStatus==WAIT_ABANDONED)
      m_isMutexAbandoned=true;
   return ret;
}   

  调用Lock函数时,无限等待直到获取互斥对象,当WaitForSingleObject函数返回时便自动获取了互斥对象。调用TryLock时,不等待直接试图获取互斥对象,而TryLockFor会在给定之间内试着获取互斥对象,更多参考信息请看 MSDN。注意:对于所有的加锁操作,会判断互斥量是否被舍弃,并重置m_isMutexAbandoned状态。因此加锁失败时,你可以调用IsMutexAbandoned查看是否是因为互斥量被舍弃引起的。

  Unlock

// Mutex.cpp
 
void Mutex::Unlock()
{
   ReleaseMutex(m_mutex);
}

9 NoLock Class

  对于锁框架而言,NoLock类在单线程环境下,只是一个占位符(我的注释:占位符的意思就是不包含具体的操作函数,只是为了保持与其它环境下比如多线程,多进程的使用一致性,可参见后面的示例代码)。我们的Nolock类继承于 BaseLock类,如下:

// NoLock.h
#include "BaseLock.h"
 
class NoLock :public BaseLock
{
public:
   NoLock();
   NoLock(const NoLock& b);
   virtual ~NoLock();
   NoLock & operator=(const NoLock&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
private:
};   

  正如上面所说,NoLock类只是一个占位符,所以没有任何成员变量,也没有锁机制函数。

  Implementation

// NoLock.cpp
NoLock::NoLock() :BaseLock()
{
}
NoLock::NoLock(const NoLock& b):BaseLock()
{
}
NoLock::~NoLock()
{
}
bool NoLock::Lock()
{
   return true;
}
long NoLock::TryLock()
{
   return 1;
}
long NoLock::TryLockFor(const unsigned int dwMilliSecond)
{
   return 1;
}
void NoLock::Unlock()
{
}  

NoLock没有做任何事情,只是有返回值时返回真。

10 Auto-release Lock

  当进行同步操作时,匹配加锁和解锁是一件非常恼火的事情,所以创建一个拥有自动释放机制的结构体是非常有必要的。我们可以扩展BaseLock类并按照如下的代码去实现:

// BaseLock.h
 
Class BaseLock
{
public:
...
   class BaseLockObj
   {
   public:
      BaseLockObj(BaseLock *lock);
      virtual ~BaseLockObj();
      BaseLockObj &operator=(const BaseLockObj & b)
      {
         return *this;
      }
   private:
      BaseLockObj();
      BaseLockObj(const BaseLockObj & b){assert(0);m_lock=NULL;}
      /// The pointer to the lock used.
      BaseLock *m_lock;
   }; 
   ... 
};  
/// type definition  for lock object 
typedef BaseLock::BaseLockObj LockObj; 

  重载拷贝操作函数,并设为私有,是为了防止从其它对象进行拷贝。缺省的构造函数被设为私有,所以必须通过传递指向BaseLock对象的指针来调用构造函数。

全局声明 BaseLock::BaseLockObjLockObj,便于访问。


  Implementation

// BaseLock.cpp 
 
BaseLock::BaseLockObj::BaseLockObj(BaseLock *lock)
{
   assert(lock); 
   m_lock=lock;
   if(m_lock) 
      m_lock->Lock();
} 
 
BaseLock::BaseLockObj::~BaseLockObj()
{
   if(m_lock)
   {
      m_lock->Unlock();
   }
} 
 
BaseLock::BaseLockObj::BaseLockObj()
{ 
   m_lock=NULL;
} 

  如代码所示,当BaseLockObj 被创建时,调用构造函数并执行Lock,自动加锁。

同理,析构时,解锁被调用执行。

这些实现对于所有的同步基元都适用,包括 NoLock,因此它们拥有了统一的接口,并且都继承自BaseLock类。

示例:

...
BaseLock *someLock = new CriticalSectionEx(); // declared in somewhere accessible
...
void SomeThreadFunc()
{ 
   ...
   if (test==0)
   {
      LockObj lock(someLock);
      // Lock is obtained automatically
      ... 
   } /// When leaving the if statement lock object is destroyed, so the Lock is automatically released
   ...    
} 
... 

11 Use-case Examples

  • 声明同步对象
 ... 
   // MUTEX 
   BaseLock * pLock = new Mutex (); 
   // OR 
   Mutex cLock; 
   ...
   // SEMAPHORE
   BaseLock * pLock = new Semaphore();
   // OR 
   Semaphore cLock;
   ...
   // CRITICAL SECTION
   BaseLock * pLock = new CriticalSectionEx();
   // OR
   CriticalSectionEx cLock;
   ...
   // NOLOCK
   BaseLock *pLock = new NoLock();
   // OR
   NoLock cLock;  
   ... 

  同一应用程序,对于不同的环境比如单线程,多线程,多进程都可以这样声明处理过程:

...

BaseLock *pLock=NULL; 
#if SINGLE_THREADED
pLock = new NoLock();
#elif MULTI_THREADED
pLock = new CriticalSectionEx();
#else // MULTI_PROCESS
pLock = new Mutex();
#endif
...   
  • 声明信号量对象,附带初始锁计数

...
BaseLock * pLock = new Semaphore(2);
// OR
Semaphore cLock(2);
...   
  • 声明用于跨进程的互斥量/信号量

... 
BaseLock * pLock = new Mutex (_T("MutexName")); 
//OR  
Mutex cLock(_T("MutexName"));
...
 
// For semaphore, lock count must be input, in order to give semaphore name 
BaseLock * pLock = new Semaphore(1, _T("SemaphoreName")); 
//OR 
Semaphore cLock(1, _T("SemaphoreName"));  
... 

对于信号量,此时应该给定初始计数值,然后设定名称。

12 示例1 (手动加锁)

void SomeFunc(SomeClass *sClass)
{ 
   ... 
   pLock->Lock();   //OR cLock.Lock();
   ... 
   pLock->Unlock(); //OR cLock.Unlock();
} 

13 示例2 (自动加锁)

void SomeFunc()
{ 
   LockObj lock(pLock); //OR LockObj lock(&cLock);
   ...
} // Lock is auto-released when exiting the function.

或者这样:

void SomeFunc(bool doSomething)
{ 
   ... 
   if(doSomething)
   { 
      LockObj lock(pLock); //OR LockObj lock(&cLock);
      ...
   }// Lock is auto-released when exiting the if-statement.
   ...
}  

14 示例3(TryLock/TryLockFor)

... 
if(pLock->TryLock()) //OR cLock.TryLock()
{
   // Lock obtained
   ...
   pLock->Unlock(); //OR cLock.Unlock();
}  
... 
或者:
...
if(pLock->TryLockFor(100)) //OR cLock.TryLockFor(100) // TryLock for 100 millisecond
{
   // Lock obtained
   ...
   pLock->Unlock(); //OR cLock.Unlock();
}  

15 更多示例

  更多的例子可以查看 EpServerEngine 的源代码。更多的细节可以看这篇文章:

("EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock" )  


16 Reference

 

License

This article, along with any associated source code and files, is licensed under The MIT License

********************************************************************************

原文:http://www.codeproject.com/Articles/421976/How-to-create-a-Simple-Lock-Framework-for-Cplusplu

 源代码:http://files.cnblogs.com/wb-DarkHorse/SimpleLockFramework.zip

原文地址:https://www.cnblogs.com/wb-DarkHorse/p/3284220.html