一个最简单的例子,在多线程环境中,我们会常常使用到”引用计数”的情况,如变量
int g_ref = 0;
全局变量g_ref的读写是多线程不安全的,这是因为相关操作是一个read-modify-write过程,因此需要使用同步机制,如考虑Critical Section、Mutex,于是有如下最直观的两种解决方案:
(1)
CRITICAL_SECTION g_cs;
EnterCriticalSection(&g_cs);
g_ref++;
LeaveCriticalSection(&g_cs);
或者
(2)
HANDLE g_mutex;
WaitForSingleObject(g_mutex, INFINITE);
g_ref++;
ReleaseMutex(g_mutex);
对这两种方案进行比较,发现(2)总是需要陷入内核,从而选择第(1)种
-----------------------------------------------------------------------------------
然而事情并未到这儿结束,我们还可以深入一步!
引用计数相关操作通常可认为是一个原子操作(Atomic Operation),于是可以使用InterLocked API改写(1)
InterLockedIncrement((LONG volatile*)&g_ref);
其中InterLockedIncrement的原型为
LONG __cdecl InterLockedIncrement(LONG volatile* Addend);
至于g_ref的其它操作,如g_ref--, g_ref+=n, g_ref-=n等,类似的InterLocked API如
InterLockedDecrement, InterLockedExchangeAdd
至于InterLocked为何被认为是原子操作,我们来看看反汇编代码
mov ecx,dword ptr [esp+4]
mov eax,1
lock xadd dword ptr [ecx],eax
inc eax
ret 4
其中核心代码为
lock xadd dword ptr [ecx],eax
其中令它成为原子操作的关键在于LOCK指令
----------------------------------------------------------------------------------
LOCK指令:
当某些指令被LOCK修饰时,在多核环境中,能保证在操作共享内存时能在一个原子操作内完成,此时目的操作数必须为内存操作数。
能使用LOCK修饰的指令包括:ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG。
当使用XCHG指令时,即使没显式使用LOCK,CPU也会会认其成为一个原子操作。
这种情况下,我们发现,我们能将read-modify-write在一个指令周期内完成,这就是所谓的原子操作。
上述指令的高级语言版本封装实现基本上都能找到,如微软中的InterLocked APIs。
同时,在使用高级语言的相关函数时,为了更直观一些,我们也许应该这样声明引用计数变量:
LONG volatile g_ref = 0;
其中volatile的作用是告诉编译器对g_ref的读写操作不要优化。
----------------------------------------------------------------------------------
引用计数只是一个简单的例子,更复杂的可以参考无锁的数据结构。