写在前面:今天一哥们问我,windows的临界代码是自旋还是等待,当时想了想应该是等待,后来翻了一下《Windows via C/C++》,发现还有点小意思。总结一下先。
关键代码段是指一个小代码段,在代码能够执行前,它必须独占对某些共享资源的访问权。这是让若干行代码能够“以原子操作方式”来使用资源的一种方法。所谓原子操作方式,是指该代码知道没有别的线程要访问该资源。当然,系统仍然能够抑制你的线程的运行,而抢先安排其他线程的运行。不过,在线程退出关键代码段之前,系统将不给想要访问相同资源的其他任何线程进行调度。
来看一段使用关键代码段的程序:
const int COUNT = 10;
int g_nSum = 0;
CRITICAL_SECTION g_cs;
DWORD WINAPI FirstThread(PVOID pvParam) {
EnterCriticalSection(&g_cs);
g_nSum = 0;
for (int n = 1; n <= COUNT; n++) {
g_nSum += n;
}
LeaveCriticalSection(&g_cs);
return(g_nSum);
}
DWORD WINAPI SecondThread(PVOID pvParam) {
EnterCriticalSection(&g_cs);
g_nSum = 0;
for (int n = 1; n <= COUNT; n++) {
g_nSum += n;
}
LeaveCriticalSection(&g_cs);
return(g_nSum);
}
int g_nSum = 0;
CRITICAL_SECTION g_cs;
DWORD WINAPI FirstThread(PVOID pvParam) {
EnterCriticalSection(&g_cs);
g_nSum = 0;
for (int n = 1; n <= COUNT; n++) {
g_nSum += n;
}
LeaveCriticalSection(&g_cs);
return(g_nSum);
}
DWORD WINAPI SecondThread(PVOID pvParam) {
EnterCriticalSection(&g_cs);
g_nSum = 0;
for (int n = 1; n <= COUNT; n++) {
g_nSum += n;
}
LeaveCriticalSection(&g_cs);
return(g_nSum);
}
当一个线程进入关键代码段,其它请求进入关键代码段的线程就会进入等待状态,这意味着该线程必须从用户方式转入内核方式(大约1000个C P U周期),这种转换是要付出很大代价的。 而对于多CPU系统,有时候这是没有必要的,实际上拥有资源的线程可以在另一个线程完成转入内核方式之前释放资源。
所以,为了提高性能,Microsoft将自旋锁引入关键代码段。当EnterCriticalSection函数被调用时,如果关键代码段已经被其它线程持有时,它就原地自旋,当自旋一定次数后还不能获取关键代码段,此时线程才转入内核方式,进入等待状态。
若要将自旋锁用于关键代码段,应该调用下面的函数,以便对关键代码段进行初始化:
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs, DWORD dwSpinCount);
其中,第二个参数dwSpinCount,传递的是在使线程等待之前它试图获得资源时自旋的次数。PCRITICAL_SECTION pcs, DWORD dwSpinCount);
Linux的pthread对于线程提供了三种机制,互斥量(mutex)(功能大概相当于Windows的关键代码段),同时为了提高并发性,还引入了读写锁,此外,还提供了条件变量(相应的介绍见《Unix环境高级编程》)。
由于windows的关键代码段工作在用户态, 所以开销应该要小于Linux互斥量。不知道为什么,pthread没有提供自旋锁机制,仅管内核大量的使用自旋锁。