翻譯自codeproject上面的一篇文章,題目是:如何創建一個簡單的c++同步鎖框架
目錄
-
介紹
-
背景
-
臨界區 & 互斥 & 信號
-
臨界區
-
互斥
-
信號
-
更多信息
-
-
建立鎖框架的目的
-
BaseLock類
-
臨界區類
-
構造/拷貝構造
-
析構
-
Lock/TryLock
-
TryLockFor
-
解鎖
-
-
信號量類
-
構造/拷貝構造
-
析構
-
Lock/TryLock/TryLockFor
-
解鎖
-
釋放
-
-
互斥類
-
構造/拷貝構造
-
析構
-
Lock/ TryLock/TryLockFor
-
解鎖
-
-
無鎖類
-
實現
-
-
自動釋放鎖類
-
實現
-
-
例子
-
聲明同步對象
-
聲明帶初始計數的信號量對象
-
聲明跨進程的互斥/信號量對象
-
同步示例1 (手動加鎖)
-
同步示例2 (自動加鎖)
-
同步示例3 (TryLock/TryLockFor)
-
-
更多的示例
-
結論
-
參考
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
為了限制訪問共享資源的線程數量,我們需要使用信號量。信號量是內核對象,它有一個計數變量用於表示當前正在使用共享資源的線程數。舉個例子,下面的代碼用MFC的CSemaphore類創建了一個信號量,它能保證在設定的時間周期(這個值有第一個參數設定)內能夠同時訪問共享資源的最大線程數量,並且初始化完成后的時刻沒有線程對資源擁有所有權(有第二個參數設定)。
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
個:semName
和lpsaAttributes
,而
BaseLock和 CriticalSectionEx的構造函數沒有。另外它的構造函數調用時可以不加參數,因為有默認參數,這幾個參數的意義如下:
下面的函數是信號量的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::BaseLockObj為 LockObj,便於訪問。
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
-
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