測試環境Windows XP SP2,,Intel Core2 Duo E8400 3.00GHz,1.94GB內存
首先使用CRITICAL_SECTION和自旋機制實現:
CRITICAL_SECTION GCritical;
......
InitializeCriticalSectionAndSpinCount(&GCritical, 0x80000FA0);
......
// 線程核心函數,共使用兩個線程
for (int i = 0; i < 100000000; ++i)
{
EnterCriticalSection(&GCritical);
++GlobalSum;
LeaveCriticalSection(&GCritical);
}
......
DeleteCriticalSection(&GCritical);
實測時間為21.578s
使用CRITICAL_SECTION和非自旋機制:
CRITICAL_SECTION GCritical;
......
InitializeCriticalSection(&GCritical);
......
// 線程核心函數,共使用兩個線程
for (int i = 0; i < 100000000; ++i)
{
EnterCriticalSection(&GCritical);
++GlobalSum;
LeaveCriticalSection(&GCritical);
}
......
DeleteCriticalSection(&GCritical);
運行時間太長,在運行2分鐘后仍然未運行完畢
可見在大規模并行運算的環境中,如果存在臨界區,自旋鎖的時間開銷遠小于普通的鎖機制。相比于普通鎖,自旋鎖在進入操
作系統內核態之前會進行多次循環,并在循環中嘗試獲取鎖,如果在循環過程中自旋鎖獲得了鎖則自旋鎖所在線
程不必由用戶態陷入內核態,也不必交出自己的時間片進入等待狀態(見操作系統原理)。從而大大減少的獲取鎖的時間
有沒有比自旋鎖開銷更小的鎖機制呢?答案是有,下面就是一個利用X86 Interlock系列指令實現的win32用戶態鎖:
class CLocker
{
private:
_declspec(align(4)) volatile long m_lThreadId;
volatile long m_lLockCount;
int m_iCollision;
public:
CLocker()
:m_lThreadId(0)
,m_lLockCount(0)
,m_iCollision(0)
{}
~CLocker()
{
if (m_lLockCount > 0)
__debugbreak();
}
void Lock()
{
const long tId = ::GetCurrentThreadId();
if (tId == m_lThreadId)
{
++m_lLockCount;
}
else
{
int iCol = 0;
while (true)
{
++iCol;
if (_InterlockedCompareExchange(&m_lThreadId, tId, 0) == 0)
{
m_lLockCount = 1;
if (iCol > m_iCollision)
m_iCollision = iCol;
return;
}
//volatile int delay;
//const int maxDelay = (int)((rand() / (float)RAND_MAX) * 100) * 50;
Sleep(0); //for (delay = 0; delay < maxDelay; ++delay); // 注1
}
}
void Release()
{
const long tId = ::GetCurrentThreadId();
if (tId == m_lThreadId)
{
if (--m_lLockCount == 0)
{
//_InterlockedExchange(&m_lThreadId, 0);
m_lThreadId = 0; // 這里不用Interlock操作,機器字對齊時寫入為原子操作
}
}
int GetCollision()
{
return m_iCollision;
}
};
該類通過Interlock系列指令集所具有的原子操作特性互斥的將臨界區資源交給獲得鎖的線程。所有操作均在用戶態執行,邏輯簡單,最終編譯結果指令數也非常少。極大提高了加鎖效率。注1處的循環是一個關鍵點。_InterlockedCompareExchange操作如果失敗會導致CPU執行復雜的Cache操作,從而浪費大量時間。使CAS失敗的線程等待,減少多線程爭用同一鎖時產生過多的_InterlockedCompareExchange操作失敗,從而提高加鎖效率。以下是測試結果
static CLocker GLocker;
......
// 線程核心函數,共兩個線程
for (int i = 0; i < 100000000; ++i)
{
GLocker.Lock();
++GlobalSum;
GLocker.Release();
}
......
測試結果為5.969s,可見用戶態鎖確實具有相對較高的效率。
需要說明的是,我的本意只是為了探討不同種類鎖的效率,實際情況中,多線程編程要盡量減少多線程間的資源爭用,從設計上避免頻繁加鎖解鎖(如Lock-Free算法)。
補充說明,windows平臺和xbox360平臺上的CriticalSection實際上包含一個內存屏障,保證臨界區前后的讀寫操作不會越界執行(見編譯器對指令的優化和CPU的指令亂序執行),一個InterlockedXxx操作,嵌套檢查,滿足特定條件向Mutex操作退化。CriticalSection使用合適的自旋次數能提高效率。CLocker由于沒有內存屏障實際上在某些情況下會不如CriticalSection安全,使用CLocker一定需要程序員自己保證內存操作的順序性。
實際上臨界區爭用導致線程等待的時間往往大于鎖執行的時間,優化加鎖解鎖速度實際上并未解決根本問題。