翻译自codeproject上面的一篇文章,题目是:如何建立一个简单的c++同步锁框架php
介绍html
背景c++
临界区 & 互斥 & 信号git
临界区github
互斥windows
信号安全
更多信息多线程
创建锁框架的目的框架
BaseLock类函数
临界区类
构造/拷贝构造
析构
Lock/TryLock
TryLockFor
解锁
信号量类
构造/拷贝构造
析构
Lock/TryLock/TryLockFor
解锁
释放
互斥类
构造/拷贝构造
析构
Lock/ TryLock/TryLockFor
解锁
无锁类
实现
自动释放锁类
实现
例子
声明同步对象
声明带初始计数的信号量对象
声明跨进程的互斥/信号量对象
同步示例1 (手动加锁)
同步示例2 (自动加锁)
同步示例3 (TryLock/TryLockFor)
更多的示例
结论
参考
开发多线程/多进程应用时, 同步是一个很大的问题. 你须要让你的程序同时知足单线程和多线程/多进程的应用场合,固然如何管理同步又是另外一个话题了。利用锁(同步)机制不会自动帮组你解决上诉问题,可是有助于你找到解决问题的方法。这篇文章向你解释了如何构建简单锁框架,如何在你的应用中运用它。可能不少人已经开发了本身的锁框架,或者利用成熟的库好比 boost library或者 Intel TBB。可是有时候它们对于你的应用来讲可能过于臃肿,也可能并非你想要的。经过阅读本文,你可能构建符合你本身的锁框架。
本文将解释下面这些东西:
BaseLock类
Critical Section类
Semaphore类
Mutex类
NoLock类
Auto-release Lock
示例
提示: 本文既不是要讲解高级的锁框架,也不是要讲解同步基元的区别。正如题目所说,本文只会告诉你:如何构建一个简单的锁框架,你能够根据本文的框架结合你本身的须要,运用或者修改它。
这里你必须了解如下几个同步基元的概念和他们之间的区别:
Critical Section
Semaphore
Mutex
若是你对MFC中的同步类(CSingleLock/CMultiLock, CCriticalSection, CSemaphore, and CMutex)有所了解,这将有助于你理解并运用这些类来代替windows函数库去实现一个锁框架。
下面这段主要来自于这篇文章 "Synchronization in Multithreaded Applications with MFC" ,由Arman S所写,我只是引用其中的内容,全部权为他本人全部。
临界区工做于用户模式,若是须要能够进入内核模式。若是一个线程运行一段位于临界区的代码,它首先会产生一个自旋区并在一段时间的等待以后进入内核模式并等待临界区对象。实际上临界区由自旋计数和信号量组成。前者用于用户态模式下的等待,后者用于内核模式下的等待(睡眠)。在win32 API中,结构体CRITICAL_SECTION
表示了一个临界区。在MFC
中,用类CCriticalSection
表示。概念上一个临界区表示一段可运行的代码,并保证这段代码在执行过程当中不会被中断打断。这样作是为了保证一个单线程在访问共享资源时,拥有绝对占有权。
互斥与临界区同样,也是用于同步访问时保护共享资源。互斥在内核中实现,所以能够进入内核模式。互斥不只能够用于线程之间,也能够用于进程之间。通常用于不一样进程之间时,它都拥有一个唯一的名字,咱们称这种互斥为命名互斥。
为了限制访问共享资源的线程数量,咱们须要使用信号量。信号量是内核对象,它有一个计数变量用于表示当前正在使用共享资源的线程数。举个例子,下面的代码用MFC的CSemaphore类建立了一个信号量,它能保证在设定的时间周期(这个值有第一个参数设定)内可以同时访问共享资源的最大线程数量,而且初始化完成后的时刻没有线程对资源拥有全部权(有第二个参数设定)。
HANDLE g_sem = CreateSemaphore(NULL,5,5,NULL);
若是想了解更多,请阅读Arms S的这篇文章 here
在windows开发中,最多见的同步基元是Critical Section, Semaphore, 和Mutex. 对于初学者他们可能常用这些同步基元,却很难去了解同步概念自己。所以创建一个简单的锁框架的出发点是使得不一样基元拥有统一的接口和调用方式, 可是按照它们原始的机制去工做,这样更容易理解。
虽然不一样的同步基元做用不同,可是仍然有一些基本的功能共同点。
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函数返回布尔型,对于其它的基元,这个函数老是返回真。
由于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个函数将合并以下:
// CriticalSectionEx.cpp CriticalSectionEx::CriticalSectionEx() :BaseLock() { InitializeCriticalSection(&m_criticalSection); m_lockCounter=0; } CriticalSectionEx::CriticalSectionEx(const CriticalSectionEx& b):BaseLock() { InitializeCriticalSection(&m_criticalSection); m_lockCounter=0; }
// CriticalSectionEx.cpp CriticalSectionEx::~CriticalSectionEx() { assert(m_lockCounter==0); DeleteCriticalSection(&m_criticalSection); }
若是CRITICAL_SECTION
对象被初始化后,它必需要释放,因此将
DeleteCriticalSection
放在析构函数里,CriticalSectionEx
析构时CRITICAL_SECTION
将被自动释放。注意若是m_lockCounter
不为
0,这意味着加锁和解锁次数不相同,这将致使未定义行为。
// 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; }
这是很是直观的实现方式。不解释了!
// 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,不然表示解锁次数大于加锁次数。
咱们的信号量类也继承自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
个:semName
和lpsaAttributes
,而
BaseLock和 CriticalSectionEx的构造函数没有。另外它的构造函数调用时能够不加参数,由于有默认参数,这几个参数的意义以下:
下面的函数是信号量的4个经常使用操做函数:
CreateSemaphore
CloseHandle
WaitForSingleObject
ReleaseSemaphore
更多信息可参考MSDN,这4个函数合并以下:
// 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); }
这里的拷贝构造函数,复制了另外一个类的安全属性和计数值,而后建立一个新的信号量对象。
// Semaphore.cpp Semaphore::~Semaphore() { CloseHandle(m_sem); }
// 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; }
// Semaphore.cpp void Semaphore::Unlock() { ReleaseSemaphore(m_sem,1,NULL); }
如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.
// 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); }
// Mutex.cpp Mutex::~Mutex() { CloseHandle(m_mutex); }
// 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::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; }
如代码所示,当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(); }
更多的例子能够查看 EpServerEngine 的源代码。更多的细节能够看这篇文章:
("EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock" )
Synchronization in Multithreaded Applications with MFC by Arman S.
EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock by Woong Gyu La
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