SRWLock的性能与关键段旗鼓相当,建议使用SRWLock来代替关键段。SRWLock不仅更快,而且允许多个线程同时读取,对乐观锁资源来说,提高了吞吐量和可伸缩性。
追求极致性能,首先应当尝试不要共享数据,然后依次volatile读取>volatile写入>Interlocked API>SRWLock>关键段,当且仅当这些都不能满足要求的时候,再使用内核对象。
可以参考https://blog.csdn.net/opensure/article/details/46669337
Interlocked函数执行的非常快,调用一次只占用几个CPU周期(不超过50)也不需要在用户模式和内核模式下进行切换(该切换需要1000周期以上)
InterlockedIncrement() //只加一
InterlockedExchangeAdd() //可以加任意整型
InterlockedExchangeAdd64() //扩充至LongLong
InterlockedExchange() //返回unsigned
InterlockedExchangePointer() //操作void *
InterlockedCompareExchange() // 32位,与第三参数对比若相等则交换
InterlockedCompareExchangePointer() //扩展64位
InterlockedCompareExchange64() //再次扩展以处理对齐后的64位值
注意:所有线程都需要调用 InterlockedExchangeAdd()
来修改共享变量的值
// Define a global variable.
1ong g_x = 0;
DWORD WINAPI ThreadFuncl(PVOID pvParam)
{
InterlockedExchangeAdd(&g_X, 1);
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
InterlockedExchangeAdd(&g_X, 1);
return(0);
}
需要注意的是,我们必须保证传给这些函数的变量地址是经过对齐的,否者可能会==失败==
C提供了_aligned_malloc函数来分配一块对齐过的内存
void * _aligned_malloc(size_t size, size_t alignment);
其中传给alignment必须是2的整数幂次方。这里提一下高速缓存行的概念。
从上面的情况可以看出,在设计数据结构的时候,应该尽量将只读数据与读写数据分开,并具尽量将同一时间访问的数据组合在一起。这样 CPU 能一次将需要的数据读入。
使用__declspec(align(#))来对字段对齐加以控制。eg.
#define CACHE_ALIGN 32 // x86系统
Struct __a
{
Int id; // 不易变
Char name[64];// 不易变
Char __Align[CACHE_ALIGN – sizeof(int)+sizeof(name)*sizeof(name[0])%CACHE_ALIGN]
Int factor;// 易变
Int value;// 易变
Char __Align2[CACHE_ALIGN –2* sizeof(int)%CACHE_ALIGN]
} ;
BOOL g_fResourceInUse = FALSE;
void Func1()
{
while(InterlockedExchange (&g_fResourceInUse, TRUE) == TRUE)
{
Sleep(0);
}
// 这边是对资源进行访问操作
// ...
InterlockedExchange(&g_fResourceInUse, FALSE);// 执行结束后释放
}
注意了:
- 单处理器的机器上不应当使用旋转锁
- 旋转锁会耗费CPU时间
- 所有使用到旋转锁的线程都以相同的优先级运行
- 对于用到旋转锁的线程来说,我们可能想要调用SetProcessPriority
或SetProcessPriorityBoost
来禁用优先级提升(thread priority boosting)
- Sleep的时间:当一次拒绝后增加等待时间防止无谓的CPU时间浪费
使用旋转锁时,我们假定被保护的资源始终只会被占用一小段时间,这种情况下与切换到内核模式然后等待相比,旋转锁的效率会更高一点。通常我们会循环指定的次数(such as 4000)如果届时任然无法获取资源访问,再将线程切换到内核模式,并一直等到资源可供使用为止(此时它不消耗CPU时间)。++这也是关键段的实现方式++。
//先来看个栗子
const int COUNT = 10;
int g_nSum = 0;
CRITICAL_SECTION g_cs; // 定义,CRITICAL_SECTION该数据结构是不对开发者可见的
DWORD WINAPI FirstThread(PVOID pvParam)
{
EnterCriticalSection(&g_cs); // 进入关键段 注意:这边传入的是地址
g_nSum = O;
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 = O;
for (int n = 1; n <= COUNT; n++)
{
g_nSum += n;
}
LeaveCriticalSection(&g_cs);
return(g_nSum);
}
需要注意的是:
- 在使用g_cs前需要对其初始化:VOID InitializeCriticalSection(PCRITICAL_SECTION pcs)
- 当不需要g_cs时候,应当调用清理函数VOID DeleteCriticalSection(PCRITICAL_SECTION pcs)
线程挨饿(starved) 两个线程同时执行EnterCriticalSection()时,一个线程会获准访问资源,另一个线程会切换到等待状态。如果资源长时间不被释放(LeaveCriticalSection)那么长期处于等待状态的线程就在挨饿了。
事实上,等待关键段的线程是绝对不会一直挨饿的,。对于EnterCriticalSection的调用最终会超时并引发异常。超时长度由CriticalSectionTimeout决定(HKEY_LOCAL_MACHINE\SYSTEM\CURRENTCONTROLSET\CONTROL\SESSION MANGER
)
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs)不会让线程进入等待状态。
Talk is cheap,show me your code.
#include
#include
#include
#include
bool flag;
CRITICAL_SECTION g_cs;
UINT _stdcall Func1(void *dummy)
{
printf("Func1\n");
bool nRuned = true;
int n_wait = 1;
do
{
if (TryEnterCriticalSection(&g_cs) == TRUE)
{
nRuned = false;
printf("Try Succeessfully!!!\n");
if (flag == true)
printf("TRUE NOW!\n");
else
printf("FALSE NOW!\n");
LeaveCriticalSection(&g_cs); // 只有在成功进入关键段后,才需要leave
}
else
{
printf("没能进入关键段,即将执行等待 %d ms\n", n_wait *= 2);
Sleep(n_wait);
printf("等待完 %d ms 继续尝试获取\n",n_wait);
}
} while (nRuned);
return 0;
}
UINT _stdcall Func2(void *dummy)
{
printf("Func2\n");
EnterCriticalSection(&g_cs);
Sleep(5000);
printf("Func2 ended\n");
LeaveCriticalSection(&g_cs);
return 0;
}
void main()
{
unsigned long Thread1 = 0, Thread2 = 0;
UINT Func1Code = 0, Func2Code = 0;
flag = false;
InitializeCriticalSection(&g_cs);
Thread2 = _beginthreadex(NULL, 0, Func2, NULL, 0, &Func2Code);
Sleep(1000);
Thread1 = _beginthreadex(NULL, 0, Func1, NULL, 0, &Func1Code);
system("pause");
DeleteCriticalSection(&g_cs);
}
对于InitializeCriticalSection来说,如果执行失败,会抛出STATUS_NO_MEMORY异常,我们可以使用结构化异常来捕获它。
使用InitializeCriticalSectionAndSpinCount将更容易发现这个问题,当内存分配不成功时,会返回FALSE
关键段会不会使用到内核对象?答案:会,当不低于两个线程在同一时刻竞争同一个关键段的时候,关键段会在内部使用一个事件内核对象。该内核对象的创建是懒汉式的。BTW,只有在调用DeleteCriticalSection时候,系统才会释放这个事件内核对象,所以用完之后千万不应该忘记调用DeleteCriticalSection
与关键段不同的是,SRWLock允许我们区分那些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)
- 分配一个SRWLOCK结构并初始化VOID InitializeSRWLock(PSRWLOCK SRWLock);
- 写入者线程可以调用void AcquireSRWLockExclusive(PSRWLOCK SRWLock);
来获取对被保护资源的独占访问权,在访问结束后调用void ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
解除锁定
- 对于读取者线程来说,调用
void AcquireSRWLockShared(PSRWLOCK SRWLock);
void ReleaseSRWLockShared(PSRWLOCK SRWLock);
WaitForSingleObject(g_hMutex, INFINITE);
gv_value = 0;
ReleaseMutex(g_hMutex);
性能最差,等待互斥量及后来释放互斥量时候需要线程每次都在用户模式和内核模式之间进行切换。切换本身的CPU时间开销就非常大。在多线程发生争夺现象时,性能会急剧下降。
线程\微秒 | Volatile读取 | Volatile写入 | Interlocked递增 | 关键段 | SRWLock共享模式 | SRWLock独占模式 | 互斥量 |
---|---|---|---|---|---|---|---|
1 | 8 | 8 | 35 | 66 | 66 | 67 | 1060 |
2 | 8 | 76 | 153 | 268 | 134 | 148 | 11082 |
4 | 9 | 145 | 361 | 768 | 244 | 307 | 23785 |
- 写入Volatile长整型值,在双处理器上的执行双线程时候,CPU之间必须互相通信以维护高速缓存的一致性,所以8->76。当四个线程操作时,相比双线程时间翻一倍,因为工作量翻了一倍,这里还不算太糟糕,因为依旧只有两个CPU。如果CPU数量进一步增加,那么性能还会下降。因为需要在更多的CPU之间进行通信以使所有CPU的高速缓存保持一致。
- InterlockedIncrement比volatile读/写要慢的原因是CPU必须要锁定内存,因此同一时刻只有一个CPU能够访问它。使用两个线程比一个线程要慢得多的原因是必须要在两个CPU之间来回传输数据以维护高速缓存的一致性。线程数&CPU数量的增加都会使得性能降低。
- 关键段更加慢的原因是我们必须先进入再离开(两个操作),进入和离开时需要修改CRITICAL_SECTION结构中的多个字段。多线程时候效率急剧下降这是因为上下文切换增大了发生争夺现象的可能性。
- SRWLock和关键段相近,但是共享模式下由于可以同时读取,故而效率得到提升(由于代码中在获得锁之后并没有执行太多的事情,故而性能提升优势没有太明显。此外,由于多个线程之间会不断地写入锁的字段以及它保护的数据,因此各CPU必须在它们的高速缓存之间来回传输数据,这也将产生开销)。
- 独占模式下,需要执行写操作的各个线程之间是互斥的。