原子访问:Interlocked系列函数
所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。
LONG InterlockedExchangeAdd(
LONG volatile* Addend,
LONG Value
);
LONGLONG InterlockedExchangeAdd64(
LONGLONG volatile* Addend,
LONGLONG Value
);
上面两个函数:
参数Addend 为要递增或者递减的变量的地址
参数Value 为增量值,可以为负数,表示前一个变量参数递减。
LONG InterlockedExchange(
LONG volatile* Target,
LONG Value
);
LONGLONG InterlockedExchange64(
LONGLONG volatile* Target,
LONGLONG Value
);
PVOID InterlockedExchangePointer(
PVOID volatile* Target,
PVOID Value
);
上面三个函数会把第一个参数指向的内存地址的当前值,以原子的方式替换为第二个参数指定的值。
LONG InterlockedCompareExchange(
LONG volatile* Destination,
LONG Exchange,
LONG Comparand
);
PVOID InterlockedCompareExchangePointer(
PVOID volatile* Destination,
PVOID Exchange,
PVOID Comparand
);
上面这两个函数以原子的方式执行一个测试和设置操作。
函数会将参数Destination 指向的当前值与参数Comparand 的值进行比较。如果两个值相同,那么函数会将 *Destination 修改为Exchange 参数的值。否则,*Destination的值保持不变。函数会返回 *Destination 原来的值。
除了可以对整数或者布尔值进行这些原子操作外,我们还可以使用一系列的其他函数来对一种被称为Interlocked单项链表的栈进行操作。
InitializeSListHead 创建一个空栈
InterlockedPushEntrySList 在栈顶添加一个元素
InterlockedPopEntrySList 移除位于栈顶的元素并将它返回
InterlockedFlushSList 清空栈
QueryDepthSList 返回栈中元素的数量
关键段
关键段是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行控制。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程。
代码如下:
#include <windows.h>
#include <stdio.h>
const int COUNT = 10;
int g_nSum = 0;
//定义CRITICAL_SECTION 结构
CRITICAL_SECTION g_cs;
DWORD WINAPI ServerThread1(PVOID pvParam)
{
//开始对共享资源的访问
EnterCriticalSection(&g_cs);
g_nSum = 0;
for(int n = 1; n <= COUNT; n++)
{
Sleep(100);
g_nSum += n;
}
printf("%u\n", g_nSum);
LeaveCriticalSection(&g_cs);
return 0;
}
DWORD WINAPI ServerThread2(PVOID pvParam)
{
EnterCriticalSection(&g_cs);
g_nSum = 0;
for(int n = 1; n <= COUNT; n++)
{
Sleep(100);
g_nSum += n;
}
printf("%u\n", g_nSum);
LeaveCriticalSection(&g_cs);
return 0;
}
int main(void)
{
//对CRITICAL_SECTION 结构的内部成员进行初始化
InitializeCriticalSection(&g_cs);
HANDLE h[2];
h[0] = CreateThread(NULL, 0, ServerThread1, NULL, 0, NULL);
h[1] = CreateThread(NULL, 0, ServerThread2, NULL, 0, NULL);
WaitForMultipleObjects(2, h, TRUE, INFINITE);
//共享资源访问完毕,清理CRITICAL_SECTION 结构
DeleteCriticalSection(&g_cs);
CloseHandle(h[0]);
CloseHandle(h[1]);
return 0;
}
Ps:
1. 一般情况下,我们将CRITICAL_SECTION 结构作为全局变量来分配。
2. 任何一个线程在试图访问共享资源之前,必须对CRITICAL_SECTION 结构的内部成员进行初始化。
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
3. 当线程不在需要访问共享资源的时候,我们应该调用如下函数来清理CRITICAL_SECTION 结构。
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
4. 在对共享资源进行访问之前,必须在代码中先调用下面的函数:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
此函数在发现已经有线程在对共享资源进行访问的时候,会使用一个事件内核对象来把调用线程切换到等待状态。
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
此函数在发现共享资源正在被其他线程访问时,它会返回FALSE , 其他返回TRUE。如果不能访问共享资源,那么调用线程可以继续做其他的事情,而不用等待。如果此函数返回TRUE, 那么表示该线程正在访问共享资源。
最后必须调用LeaveCriticalSection函数来与之对应。
5. 在代码完成对共享资源的访问后,应该调用如下函数:
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
Slim读/写锁
SRWLock的目的与关键段相同:对一个资源进行保护,不让其他线程访问他。但是,不同的是,SRWLock允许我们区分只读取资源的线程和想更新资源的值的线程。
1. 我们需要分配一个SRWLOCK结构并对其进行初始化:
VOID InitializeSRWLock(PSRWLOCK SRWLock);
2. 初始化完SRWLOCK结构之后,写入者线程调用如下函数以偿试获得对保护资源的独占访问权。
VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
完成对资源的更新之后:
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
3. 对读取者线程来说,同样有两个步骤:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
条件变量
有时我们想让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立。
BOOL WINAPI SleepConditionVariableCS(
__in_out PCONDITION_VARIABLE ConditionVariable,
__in_out PCRITICAL_SECTION CriticalSection,
__in DWORD dwMilliseconds
);
BOOL WINAPI SleepConditionVariableSRW(
__in_out PCONDITION_VARIABLE ConditionVariable,
__in_out PSRWLOCK SRWLock,
__in DWORD dwMilliseconds,
__in ULONG Flags
);
1.ConditionVariable 参数指向一个已初始化的条件变量,调用线程正在等待该条件变量。
2.第二个参数是一个指向关键段或者SRWLock的指针,该关键段或SRWLock用来同步对共享资源的访问。
3. dwMilliseconds 参数表示花多少时间来等待条件变量被触发。
4.第二个函数的最后一个Flags参数用来指定一旦条件变量被触发,我们希望线程以何种方式来得到锁:对于写入者线程来说,应该传入0,表示希望独占资源的访问;对于读取者线程来说,应该传入CONDITION_VARIABLE_LOCKMODE_SHARE,表示希望共享对资源的访问。
唤醒被阻塞在Sleep*函数中的线程:
VOID WINAPI WakeConditionVariable(
__in_out PCONDITION_VARIABLE ConditionVariable
);
VOID WINAPI WakeAllConditionVariable(
__in_out PCONDITION_VARIABLE ConditionVariable
);
前一个函数唤醒一个正在等待的线程。
后一个函数可以唤醒好几个正在等待的线程,比如读取者线程,可以一同对共享资源进行读取操作。