65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (29投票s)

2012年8月3日

MIT

16分钟阅读

viewsIcon

149469

downloadIcon

1658

本文解释了如何为 C++ 同步创建自己的简单锁框架。

目录

  1. 引言
  2. 背景
  3. 临界区 vs 互斥体 vs 信号量
    1. 临界区
    2. 互斥体
    3. 信号量
    4. 更多信息。
  4. 锁框架的目的
  5. BaseLock 类
  6. 临界区类
    1. 构造函数/复制构造函数
    2. 析构函数
    3. Lock/TryLock
    4. TryLockFor
    5. Unlock
  7. 信号量类
    1. 构造函数/复制构造函数
    2. 析构函数
    3. Lock/TryLock/TryLockFor
    4. Unlock
    5. Release
  8. 互斥体类
    1. 构造函数/复制构造函数
    2. 析构函数
    3. Lock/TryLock/TryLockFor
    4. Unlock
  9. NoLock 类
    1. 实现
  10. 自动释放锁
    1. 实现
  11. 用例示例
    1. 声明同步对象
    2. 声明带初始锁计数的信号量对象
    3. 声明用于跨进程的互斥体/信号量对象
    4. 同步用法类型1(手动锁)
    5. 同步用法类型2(整个结构体锁)
    6. 同步用法类型3(TryLock/TryLockFor)
  12. 更实际的示例源代码
  13. 结论
  14. 参考

简介

在开发多线程/多进程应用程序时,同步是一个大问题。如果您必须为单线程/多线程/多进程环境构建相同的应用程序,那么管理同步对象就是另一个问题。拥有锁(同步)框架并不能自动解决所有上述问题,但它将有助于找到解决这些问题的方法。因此,本文解释了如何构建自己的简单锁(同步)框架,以及如何在开发项目中使用它们。

许多专业开发人员可能已经拥有自己的高级锁(同步)框架,或者他们可能使用著名的流行库,例如boost 库Intel TBB。然而,有时这些高级库对于您的应用程序来说可能过于杀鸡用牛刀,而且它们可能不合您的口味。阅读本文后,您将能够根据自己的喜好构建自己的锁(同步)框架。

本文将解释以下内容

  • BaseLock 类
  • 临界区类
  • 信号量类
  • 互斥体类
  • NoLock 类
  • 自动释放锁
  • 用例示例
** 警告:本文是关于高级锁框架的用例解释,也不是同步原语差异的解释。如主题所述,本文仅解释“如何构建一个简单的锁框架”,您有权根据您的项目和/或开发环境使用/更改/修改/改进所提供的框架。

背景

您至少需要理解以下同步原语的概念

  • 临界区
  • 信号量
  • 互斥体

以及它们之间的差异。

这是可选的,但了解 MFC 同步类(CSingleLock/CMultiLockCCriticalSectionCSemaphoreCMutex)将允许您使用 MFC 同步类而不是 Windows 默认库函数来实现此处提供的锁框架。

临界区 vs 互斥体 vs 信号量

本节中的信息最初来自 Arman S. 撰写的“使用 MFC 进行多线程应用程序中的同步”一文。我只是展示了他文章中的信息,以便人们可以轻松访问。因此,所有的辛勤工作和贡献都应归功于 Arman S.。

临界区

临界区在用户模式下工作,除非需要进入内核模式。如果一个线程试图运行被临界区捕获的代码,它首先会进行自旋阻塞,并在指定的时间后进入内核模式等待临界区。实际上,临界区由一个自旋计数器和一个信号量组成;前者用于用户模式等待,后者用于内核模式等待(休眠)。在 Win32 API 中,有一个表示临界区对象的CRITICAL_SECTION结构。在 MFC 中,有一个名为CCriticalSection的类。从概念上讲,临界区是需要集成执行的源代码部分,即在执行该部分代码期间,应保证执行不会被其他线程中断。在需要授予单个线程独占使用共享资源的情况下,可能需要这样的代码部分。

互斥体

互斥体与临界区一样,旨在保护共享资源免受同时访问。互斥体在内核内部实现,因此它们进入内核模式操作。互斥体不仅可以在不同线程之间执行同步,还可以在不同进程之间执行同步。这样的互斥体应该有一个唯一的名称,以便其他进程识别(这种互斥体称为命名互斥体)。

信号量

为了限制使用共享资源的线程数量,我们应该使用信号量。信号量是一个内核对象。它存储一个计数器变量,以跟踪正在使用共享资源的线程数量。例如,以下代码通过 MFC CSemaphore 类创建一个信号量,该信号量可以用于保证在给定时间段内最多只有 5 个线程能够使用共享资源(此事实由构造函数的第一个参数指示)。假设最初没有线程捕获资源(第二个参数)

HANDLE g_sem=CreateSemaphore(NULL,5,5,NULL); 

更多信息。

有关更多信息,请阅读 Arms S. 的文章此处

锁框架的目的

Windows 开发中众所周知的同步原语有临界区信号量互斥体。对于新手开发人员,他们可能很难理解同步本身的概念,而使用不同的同步原语可能过于复杂。因此,这个简单锁框架的想法是让不同的同步原语具有相同的接口,并以相同的方式使用它们,但在运行时按照其原始目的工作。

BaseLock 类

虽然不同的同步原语有其各自的用途,但它们仍然共享一些基本功能。

  • 锁定
  • TryLock
  • TryLockFor(在一定时间内尝试加锁)
  • Unlock

有些人可能会想到更多的功能,但这将是您在构建自己的框架时的选择。为了简单起见,我将只关注这四种功能。

所以我们的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函数返回布尔值;对于所有其他情况,它始终返回 true。 

临界区类

由于CRITICAL_SECTION结构已经存在,我将我们的类命名为CriticalSectionEx。我们的CriticalSectionEx类将是BaseLock类的子类,它也将是CRITICAL_SECTION对象的接口类。

因此,我们的CriticalSectionEx类将如下所示,它将与我们的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对象的值将被其他对象的值替换。我没有将其设为private的原因是,如果复制操作符是private的,当某个类 A 持有一个CriticalSectionEx对象,并且当类 A 的对象试图从其他对象复制时,将导致编译错误。
  • 另外请注意,由于Critical Section是可重入的,并且进入和离开计数器的差异会导致未定义的行为,因此添加了m_lockCounter来跟踪加锁/解锁的数量。(感谢 Orjan Westin 的建议!) 

通常使用临界区对象时有五个重要的函数需要了解。

  • InitializeCriticalSection
  • DeleteCriticalSection
  • EnterCriticalSection
  • LeaveCriticalSection
  • TryEnterCriticalSection
有关每个功能的更多详细信息,请参阅MSDN

这五个函数将如下所示:

构造函数/复制构造函数

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

上面的代码片段是构造函数和复制构造函数的实现。通过在构造函数中调用“InitializeCriticalSection”,在创建 CriticalSectionEx 对象时会自动初始化CRITICAL_SECTION对象。

  • 另请注意,复制构造函数不复制任何内容而仅初始化CRITICAL_SECTION对象的原因与“operator=”的原因相同

析构函数

// 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;
}       

这是一个非常直接的实现。当调用“Lock”函数时,它通过调用“EnterCriticalSection”函数进入临界区。当调用“TryLock”函数时,它尝试通过调用“TryEnterCriticalSection”进入临界区。(成功加锁后还会递增m_lockCounter) 

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对象实现此功能是有原因的。
  • CRITICAL_SECTION对象,这不能保证进入临界区的顺序。
此函数执行以下操作
  1. 尝试使用TryEnterCriticalSection进入临界区
    • 如果成功,返回TryEnterCriticalSection的代码
  2. 如果失败,等待CRITICAL_SECTION对象在调用者给定时间内释放
    • 如果释放,再次尝试使用TryEnterCriticalSection进入临界区。
      • 如果成功,返回TryEnterCriticalSection的代码。
      • 如果失败且有剩余时间,重复步骤 2 并使用剩余时间。
      • 如果失败且没有剩余时间,返回 0。

解锁

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

如果获得了CRITICAL_SECTION对象,则必须释放它。因此,通过在Unlock函数中调用“LeaveCriticalSection”,当调用Unlock函数时,它会离开临界区。

  • 请注意,m_lockCounter必须大于或等于 0,否则解锁计数多于加锁计数。 

信号量类

我们的Semaphore类将是BaseLock类的子类,它也将是Semaphore对象(实际上是一个HANDLE)的接口类。

所以我们的Semaphore类将如下所示,它将与我们的BaseLock类非常相似。

// 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;
};      

  • 请注意,“operator=”不执行任何操作就返回自身的原因是,我不想让Semaphore对象的值被其他对象替换。如果未实现“operator=”,当调用复制操作符时,它会自动调用默认复制操作符,并且Semaphore对象的值(例如本例中的m_semm_lpsaAttributesm_count)将被其他对象的值替换。我没有将其设为private的原因是,如果复制操作符是private的,当某个类 A 持有一个Semaphore对象,并且当类 A 的对象试图从其他对象复制时,将导致编译错误。
  • 另请注意,构造函数现在接受诸如countsemNamelpsaAttributes之类的参数,而 BaseLock 和 CriticalSectionEx 的构造函数没有参数。由于它们有默认参数,因此即使没有参数也可以使用,每个参数的详细信息如下:
    • count 表示信号量的锁计数
      • 默认值为“1”,表示二进制信号量。
    • semName 表示信号量的名称。
      • 默认值为“NULL”
      • 如果信号量将在进程之间使用,则可能需要此参数。(请参阅MSDN。)
    • lpsaAttributes 表示新信号量的安全描述符。
      • 默认值为“NULL”,表示默认安全描述符。
      • (有关更多详细信息,请参阅MSDN。)

通常使用信号量对象时有四个重要的函数需要了解。

  • CreateSemaphore
  • CloseHandle
  • WaitForSingleObject
  • ReleaseSemaphore
有关每个功能的更多详细信息,请参阅MSDN

这四个函数将如下所示:

构造函数/复制构造函数

// 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);
}  

上面的代码片段是构造函数和复制构造函数的实现。通过在构造函数中调用“CreateSemaphore”,在创建Semaphore对象时会自动创建Semaphore句柄。

  • 另请注意,在Semaphore的情况下,复制构造函数复制m_countm_lpsaAttributes,并创建新的Semaphore对象。基本上,这将具有与给定Semaphore对象相同的锁计数和安全描述符,但Semaphore对象将是一个新实例。

析构函数

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

如果创建了Semaphore句柄,则必须将其关闭。因此,通过在析构函数中调用“CloseHandle”,当Semaphore对象被删除/销毁时,Semaphore句柄将自动删除。

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;
}    

这是一个非常直接的实现。

  • 当调用“Lock”函数时,它会无限期地等待获取信号量,当“WaitForSingleObject”返回时,信号量会自动获取。
  • 当调用“TryLock”函数时,它通过等待 0 毫秒来尝试获取信号量。
  • 当调用“TryLockFor”函数时,它通过等待给定的时间来尝试获取信号量。
  • 有关更多详细信息,请参阅MSDN

解锁

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

如果获得了Semaphore,则必须释放它。因此,通过在Unlock函数中调用“ReleaseSemaphore”,当调用Unlock函数时,Semaphore句柄可以被释放。

释放  

正如 Ahmed Charfeddine 建议的那样,对于信号量,“重要的是 Unlock 会接受一个释放计数参数并返回成功/失败状态。”由于这是信号量特定的函数,您可以简单地通过引入一个新函数“Release”来扩展 Semaphore 类。

// 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);   

(感谢 Ahmed Charfeddine 的建议!)

互斥体类

我们的Mutex类将是BaseLock类的子类,它也将是Mutex对象(实际上是一个HANDLE)的接口类。

所以我们的Mutex类将如下所示,它将与我们的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;
};     
  • 请注意,“operator=”不执行任何操作就返回自身的原因是,我不想让Mutex对象的值被其他对象替换。如果未实现“operator=”,当调用复制操作符时,它会自动调用默认复制操作符,并且Mutex对象的值(例如本例中的m_mutexm_lpsaAttributes)将被其他对象的值替换。我没有将其设为private的原因是,如果复制操作符是private的,当某个类 A 持有一个Mutex对象,并且当类 A 的对象试图从其他对象复制时,将导致编译错误。
  • 另请注意,构造函数现在接受诸如mutexNamelpsaAttributes之类的参数,这与Semaphore的构造函数类似。由于它们有默认参数,因此即使没有参数也可以使用,每个参数的详细信息如下:
    • mutexName 表示信号量的名称。
      • 默认值为“NULL”
      • 如果互斥体将在应用程序之间使用,则可能需要此参数。(请参阅MSDN。)
    • lpsaAttributes 表示新信号量的安全描述符。
      • 默认值为“NULL”,表示默认安全描述符。
      • (有关更多详细信息,请参阅MSDN。)
  • 请注意,Mutex可能会被放弃。但是,我不想仅仅为了Mutex而更改接口,所以我添加了一个函数IsMutexAbandoned来检查当锁定失败时Mutex是否被放弃。(感谢 Orjan Westin 的建议。) 

通常使用互斥体对象时有四个重要的函数需要了解。

  • CreateMutex
  • CloseHandle
  • WaitForSingleObject
  • ReleaseMutex
有关每个功能的更多详细信息,请参阅MSDN

这四个函数将如下所示:

构造函数/复制构造函数

// 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);
}    

上面的代码片段是构造函数和复制构造函数的实现。通过在构造函数中调用“CreateMutex”,在创建Mutex对象时会自动创建Mutex句柄。

  • 另请注意,在Mutex的情况下,复制构造函数复制m_lpsaAttributes,并创建新的Mutex对象。基本上,这将具有与给定 Mutex 对象相同的安全描述符,但Mutex对象将是一个新实例。

析构函数

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

如果创建了Mutex句柄,则必须将其关闭。因此,通过在析构函数中调用“CloseHandle”,当Mutex对象被删除/销毁时,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”函数时,它通过等待 0 毫秒来尝试获取互斥体。
  • 当调用“TryLockFor”函数时,它通过等待给定的时间来尝试获取互斥体。
  • 有关更多详细信息,请参阅MSDN
  • 请注意,对于所有加锁函数,它都会检查Mutex是否被放弃,并设置成员标志变量m_isMutexAbandoned以表示状态。因此,当加锁失败时,您可以调用“IsMutexAbandoned”函数来检查失败是否是由于Mutex被放弃造成的。 

解锁

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

如果获得了Mutex,则必须释放它。因此,通过在Unlock函数中调用“ReleaseMutex”,当调用Unlock函数时,Mutex句柄可以被释放。 

NoLock 类

NoLock类就像单线程环境中锁框架的占位符。当为单线程、多线程、多进程等多个环境构建相同的应用程序时,NoLock 将充当单线程环境的占位符。我们的Nolock类将是BaseLock类的子类。

所以我们的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类只代表一个占位符,因此它没有任何成员变量,也没有任何锁机制功能。

实现将如下所示,

实现

// 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类不执行任何操作,只是在需要时返回 true。

自动释放锁

在开发同步时,有时“Lock”和“Unlock”的匹配非常烦人。因此,为结构体创建一个自动释放机制在这种情况下会非常有帮助。通过扩展“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::BaseLockObj声明为全局类型定义LockObj,以便于访问。

实现

// 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;
} 
  • 因此,通过在构造函数中调用“Lock”,当创建BaseLockObj时,它会自动加锁。
  • 由于在BaseLockObj的析构函数中调用了“UnLock”,因此当BaseLockObj被销毁时,它会自动解锁。
这可以跨同步原语使用,包括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
   ...    
} 
... 
  • 如上例所示,在 if 语句中创建一个局部变量(本例中为lock)作为LockObj对象,并带有指向CriticalSectionEx对象的someLock指针,然后临界区将自动进入。
  • 由于我们在LockObj的析构函数中实现了“Unlock”,并且根据 C++ 机制,当离开 if 语句时,局部变量“lock”将自动删除,并自动离开临界区。

用例示例

声明同步对象

   ... 
   // 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"));  
... 
  • 请注意,对于Semaphore,必须输入初始锁计数才能给出Semaphore的名称。

同步用法类型1(手动锁)

void SomeFunc(SomeClass *sClass)
{ 
   ... 
   pLock->Lock();   //OR cLock.Lock();
   ... 
   pLock->Unlock(); //OR cLock.Unlock();
} 
  • 这是通常直接的加锁和解锁用法。

同步用法类型2(整个结构体锁)

这是一个用例示例,它使用了自动释放锁机制(LockObj)。

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.
   ...
}  

同步用法类型3(TryLock/TryLockFor)

... 
if(pLock->TryLock()) //OR cLock.TryLock()
{
   // Lock obtained
   ...
   pLock->Unlock(); //OR cLock.Unlock();
}  
... 

或者对于 TryLockFor(在一定时间内尝试加锁)

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

更实际的示例源代码

可以从EpServerEngine源代码中找到更实际的示例。

(更多详情请参阅“EpServerEngine - 一个使用 C++ 和 Windows Winsock 的轻量级模板服务器客户端框架”文章。)  

结论

正如我在引言中所说,有许多高级锁框架库,例如boost 库Intel TBB。然而,在许多情况下,它们可能有点过于庞大,不适合您的项目。这是一个非常简单的指南,向您展示如何构建自己的锁框架。您可以根据自己的需求或喜好更改或扩展功能。希望这有助于您痛苦的同步开发。

参考

历史

  • 2013年8月22日:- 根据 MIT 许可证重新分发  
  • 2012年11月16日:- 根据建议进行了小幅更新,并更新了源代码  
  • 2012年9月21日:- 更新了目录 
  • 2012年8月10日:- 添加了新章节
  • “背景”
  • “临界区 vs 互斥体 vs 信号量”
  • 2012年8月4日:- 将文章的子章节移至“操作指南”
  • 2012年8月3日:- 提交了文章。
© . All rights reserved.