如何为 C++ 同步创建一个简单的锁框架
本文解释了如何为 C++ 同步创建自己的简单锁框架。
目录
简介
在开发多线程/多进程应用程序时,同步是一个大问题。如果您必须为单线程/多线程/多进程环境构建相同的应用程序,那么管理同步对象就是另一个问题。拥有锁(同步)框架并不能自动解决所有上述问题,但它将有助于找到解决这些问题的方法。因此,本文解释了如何构建自己的简单锁(同步)框架,以及如何在开发项目中使用它们。
许多专业开发人员可能已经拥有自己的高级锁(同步)框架,或者他们可能使用著名的流行库,例如boost 库或Intel TBB。然而,有时这些高级库对于您的应用程序来说可能过于杀鸡用牛刀,而且它们可能不合您的口味。阅读本文后,您将能够根据自己的喜好构建自己的锁(同步)框架。
本文将解释以下内容
- BaseLock 类
- 临界区类
- 信号量类
- 互斥体类
- NoLock 类
- 自动释放锁
- 用例示例
背景
您至少需要理解以下同步原语的概念
- 临界区
- 信号量
- 互斥体
以及它们之间的差异。
这是可选的,但了解 MFC 同步类(CSingleLock/CMultiLock、CCriticalSection、CSemaphore 和 CMutex)将允许您使用 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
这五个函数将如下所示:
构造函数/复制构造函数
// 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
对象,这不能保证进入临界区的顺序。
- 尝试使用
TryEnterCriticalSection
进入临界区 - 如果成功,返回
TryEnterCriticalSection
的代码 - 如果失败,等待
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_sem
、m_lpsaAttributes
和m_count
)将被其他对象的值替换。我没有将其设为private
的原因是,如果复制操作符是private
的,当某个类 A 持有一个Semaphore
对象,并且当类 A 的对象试图从其他对象复制时,将导致编译错误。 - 另请注意,构造函数现在接受诸如
count
、semName
、lpsaAttributes
之类的参数,而 BaseLock 和 CriticalSectionEx 的构造函数没有参数。由于它们有默认参数,因此即使没有参数也可以使用,每个参数的详细信息如下:
通常使用信号量对象时有四个重要的函数需要了解。
- CreateSemaphore
- CloseHandle
- WaitForSingleObject
- ReleaseSemaphore
这四个函数将如下所示:
构造函数/复制构造函数
// 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_count
和m_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_mutex
和m_lpsaAttributes
)将被其他对象的值替换。我没有将其设为private
的原因是,如果复制操作符是private
的,当某个类 A 持有一个Mutex
对象,并且当类 A 的对象试图从其他对象复制时,将导致编译错误。 - 另请注意,构造函数现在接受诸如
mutexName
、lpsaAttributes
之类的参数,这与Semaphore
的构造函数类似。由于它们有默认参数,因此即使没有参数也可以使用,每个参数的详细信息如下: - mutexName 表示信号量的名称。
- 默认值为“NULL”
- 如果互斥体将在应用程序之间使用,则可能需要此参数。(请参阅MSDN。)
- lpsaAttributes 表示新信号量的安全描述符。
- 默认值为“NULL”,表示默认安全描述符。
- (有关更多详细信息,请参阅MSDN。)
- 请注意,
Mutex
可能会被放弃。但是,我不想仅仅为了Mutex
而更改接口,所以我添加了一个函数IsMutexAbandoned
来检查当锁定失败时Mutex
是否被放弃。(感谢 Orjan Westin 的建议。)
通常使用互斥体对象时有四个重要的函数需要了解。
- CreateMutex
- CloseHandle
- WaitForSingleObject
- ReleaseMutex
这四个函数将如下所示:
构造函数/复制构造函数
// 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。然而,在许多情况下,它们可能有点过于庞大,不适合您的项目。这是一个非常简单的指南,向您展示如何构建自己的锁框架。您可以根据自己的需求或喜好更改或扩展功能。希望这有助于您痛苦的同步开发。
参考
- EpLibrary 2.0
- EpServerEngine
- 互斥锁(来自维基百科)
- 信号量(来自维基百科)
- 来自 MSDN 的临界区
- 来自 MSDN 的互斥体
- 来自 MSDN 的信号量
- Intel Threading Building Blocks 开源版
- Boost.Thread
- Arman S. 撰写的《使用 MFC 在多线程应用程序中进行同步》
- Woong Gyu La 撰写的《EpServerEngine - 一个使用 C++ 和 Windows Winsock 的轻量级模板服务器客户端框架》
历史
- 2013年8月22日:- 根据 MIT 许可证重新分发
- 2012年11月16日:- 根据建议进行了小幅更新,并更新了源代码
- 2012年9月21日:- 更新了目录
- 2012年8月10日:- 添加了新章节
- “背景”
- “临界区 vs 互斥体 vs 信号量”
- 2012年8月4日:- 将文章的子章节移至“操作指南”
- 2012年8月3日:- 提交了文章。