一、SRWLock锁的工作原理
SRWLock锁的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。但是,与关键段不同的是:SRWLock锁允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有的读取者线程在同一时刻访问共享资源应该是可行的,这是因为读取资源并不存在破坏资源的风险。只有当写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下:写入者线程独占对资源的访问权。
二、SRWLock锁的用法
首先,需要分配一个SRWLOCK结构并进行初始化:
- VOID WINAPI InitializeSRWLock(
- _Out_ PSRWLOCK SRWLock
- );
对于SRWLOCK结构的具体内容,我们没有必要去详细了解。
写入者进程的操作
一旦SRWLock的初始化完成之后,写入者线程就可以调用AcquireSRWLockExclusive,将SRWLOCK对象的地址作为参数传入,以尝试获得对被保护资源的独占访问权。
- VOID WINAPI AcquireSRWLockExclusive(
- _Inout_ PSRWLOCK SRWLock
- );
在完成对资源的更新之后。应该调用ReleaseSRWLockExclusive函数,并将SRWLOCK对象的地址作为参数传入,解除对资源的锁定。
- VOID WINAPI ReleaseSRWLockExclusive(
- _Inout_ PSRWLOCK SRWLock
- );
读取者的操作
读取者调用的两个参数是:
- VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
- VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
不存在删除或销毁SRWLock的函数,系统会自动执行清理工作。
与关键段相比,SRWLock缺乏下面两个特性:
(1)不存在TryEnter(Shared/Exclusive)SRWLock之类的函数。如果锁已经被占用,那么调用AcquireSRWLock(SHared/Exclusive)会阻塞调用线程。
(2)不能递归调用SRWLOCK。一个线程不能为了多次写入资源而多次锁定资源,然后多次调用ReleaseSRWLock来释放对资源的锁定。
但是,如果可以接受这些限制,就可以用SRWLock来代替关键段,并获得实际性能和可伸缩性的提升。
三、同步机制性能的比较
通过一个简单的基准测试可以比较各种同步机制的性能:产生1、2和4个线程,使用不同的同步机制重复执行相同的任务,在双处理器上运行,得出的结果是:
用户模式下同步机制性能的对比实验结果如下(计数单位:微秒)
线程数 |
Volatile Read |
Volatile Write |
Interlocked Increment |
Critical Section |
SRWLock Shared |
SRWLock Exclusive |
Mutex |
1 |
8 |
8 |
35 |
66 |
66 |
67 |
1060 |
2 |
8 |
76 |
153 |
268 |
134 |
148 |
11082 |
4 |
9 |
145 |
361 |
768 |
244 |
307 |
23785 |
各种机制对比:
(1)读取volatile长整型值,读取非常快,因为不需要进行任何同步,与CPU的高速缓存完全无关。
(2)写入volatile长整型值。单线程的时间和读取差不多,但是双线程的时候时间不只是加倍,这是因为CPU之间必须相互通信以维护高速缓存的一致性。如果机器有更多的CPU,那么性能还会下降,因为需要在更多的CPU之间进行通信来使得所有CPU的高速缓存一致。
(3)使用InterlockedIncrement来安全递增一个volatile长整型值。
它比第一种方法要慢,这是因为CPU必须锁定内存。使用两个线程要比一个线程慢得多,这是因为必须在两个CPU之间来回传输数据以维护高速缓存的一致性。
(4)使用关键段来读取一个volatile长整型值。
关键段比较慢,是因为我们必须先进入再离开。进入和离开需要修改CRITICAL_SECTION结果中的多个字段。4个线程需要花费更多时间,是因为上下文切换增大了发生争夺现象的可能性。
(5)使用SRWLock来读取一个volatile长整型值。
当有多个线程的时候,读操作比写操作快。由于多个线程会不断地写入锁的字段以及它保护的数据,因此各CPU必须在它们的高速缓存之间来回传输数据。
它的性能和关键段差不多,但是很多时候要优于关键段。建议的做法是用SRWLock替代关键段。
(6)使用同步内核对象互斥量。
互斥量是目前性能最差的,是因为等待互斥量以及后来释放互斥量需要线程每次在用户模式和内核模式之间却换,开销很大。
总结:应该首先尝试不要共享数据,然后依次使用volatile读取、volatile写入、Interlocked函数、SRWLock以及关键段。仅当这些都不能满足要求的时候,再使用内核对象。
实例代码:
#include <windows.h>
#include <windowsx.h>
#include <process.h>
#include <iostream>
using namespace std;
DWORD WINAPI FirstThread(PVOID pvParam);
DWORD WINAPI SecondThread(PVOID pvParam);
const long Count = 10000;
long g_nSum = 0;
SRWLOCK g_SRWLock; //定义SRWLOCK结构
int main()
{
InitializeSRWLock(&g_SRWLock) ;
HANDLE hThread1 = CreateThread(NULL,0,FirstThread,NULL,0,0);
HANDLE hThread2 = CreateThread(NULL,0,SecondThread,NULL,0,0);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(5000);
return 0;
}
DWORD WINAPI FirstThread(PVOID pvParam)
{
//执行写操作
AcquireSRWLockExclusive(&g_SRWLock);
cout << "Thread1 get the SRWLock , and do writing" << endl;
g_nSum = 0;
for(int i = 0;i<1000;++i)
{
g_nSum = 0;
for(int n=1; n<=Count; ++n)
{
g_nSum += n;
}
}
ReleaseSRWLockExclusive(&g_SRWLock);
return 0;
}
DWORD WINAPI SecondThread(PVOID pvParam)
{
//执行读操作
AcquireSRWLockShared(&g_SRWLock);
cout << "Thread1 get the SRWLock , and do reading" << endl;
cout << "g_num = " << g_nSum << endl;
ReleaseSRWLockShared(&g_SRWLock);
return 0;
}