几种线程锁的效率比较(多线程,锁,CRITICAL_SECTION)
测试环境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一定需要程序员自己保证内存操作的顺序性。
实际上临界区争用导致线程等待的时间往往大于锁执行的时间,优化加锁解锁速度实际上并未解决根本问题。