2. 旋转锁
// 旋转锁伪代码
// Global variable indicating whether a shared resource is in use or not
BOOL g_fResourceInUse = FALSE;
void Func1(){
// Wait to access the resource.
while( InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE )
{
sleep(0);
}
// Access the resource
// We no longer need to access the resource.
InterlockedExchange(&g_fResourceInUse,FALSE);
} while 循环不停地运行,把 g_fResourceInUse 的值设为 TRUE 并检查原来的值是否为 TRUE。如果原来的值为 FALSE,那说明资源尚未被使用,于是调用线程立刻就能将它设为“使用中”,然后退出循环。如果原来的值是 TRUE,那说明有其他的线程正在使用该资源,于是 while 循环会继续执行。
对那些用到旋转锁的线程来说,我们可能想要(调用 SetProcessPriorityBoost 或 SetThreadPriorityBoost 来)禁用线程优先级提升(thread priority boosting)。
在单 CPU 的机器上应该避免使用旋转锁
。如果一个线程不停地循环,那么这不仅会浪费宝贵的 CPU 时间,而且会阻止其他线程改变锁的值。旋转锁假定被保护的资源始终只会被占用一小段时间。与切换到内核模式然后等待相比,在这种情况下以循环的方式进行等待的效率会更高。 当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。
3. 关键段和旋转锁
为了提高关键段的性能,Microsoft 把旋转锁合并到了关键段中。因此,当调用 EnterCriticalSection 的时候,它会用一个旋转锁不断地循环,尝试在
一段时间内获得对资源的访问权。只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。
为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段:
BOOL InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);
// pcs,关键段结构的地址
// dwSpinCount,旋转锁循环的次数
在把线程切换到等待状态之前,函数会先尝试用旋转锁来获得对资源的访问权。这个值可以是从0到0x00FFFFFF之间的任何一个值。
如果我们在单处理器的机器上调用这个函数,那么函数会忽略 dwSpinCount 参数,因此次数总是为0。
我们可以调用下面的函数来改变关键段的旋转次数:
DWORD SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);
// 如果主机只有一个单处理器,函数会忽略 dwSpinCount 参数
我们应该总是在使用关键段的时候同时使用旋转锁,因为这样做不会损失任何东西。
用来保护进程堆的关键段所使用的旋转次数大约是4000。
4. Slim 读/写锁
SRWLock 的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。但是,与关键段不同的是,SRWLock 允许我们区分那些想要读取资源的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。
提供的功能:
让所有的读取者线程在同一时刻访问共享资源应该是可行的,这是因为仅仅读取资源的值并不存在破坏数据的风险。
只有当写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下,写入者线程应该独占对资源的访问权:任何其他线程,无论是读取者线程还是写入者线程,都不允许访问资源。
使用 SRWLock:
首先,分配一个 SRWLOCK 结构并用 InitializeSRWLock 函数对它进行初始化:
VOID InitializeSRWLock(PSRWLOCK SRWLock);
一旦 SRWLock
的初始化完成之后,写入者线程就可以调用 AcquireSRWLockExclusive,将 SRWLock 对象的地址作为参数传入,以尝试获得对被保护的资源的
独占访问权。
VOID AequireSRWLockExclusive(PSRWLOCK SRWLock);
完成对资源的更新之后,应该调用 ReleaseSRWLockExclusive,并将 SRWLOCK 对象的地址作为参数传入,这样就解除了对资源的锁定。
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
对于读取者线程来说同样有两个类似的步骤:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
如果锁已经被占用,那么调用 AcquireSRWLock(Shared/Exclusive) 会阻塞调用线程。
不能递归地获得 SRWLOCK。即,一个线程不能为了多次写入资源而多次锁定资源,然后再多次调用 ReleaseSRWLock* 来释放对资源的锁定。
不存在用来删除或销毁 SRWLOCK 的函数,系统会自动执行清理工作。
5. 条件变量
有时我们想让
线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止。要实现这样的线程同步是比较复杂的。
Windows 通过 SleepConditionVariableCS 或 SleepConditionVariableSRW 函数,通过了一种
条件变量,来帮助我们简化这种情形下所需的工作。
BOOL SleepConditionVariableCS(PCONDITION_VARIABLE pConditionVariable,PCRITICAL_SECTION pCriticalSection,DWORD dwMilliseconds);
BOOL SleepConditionVariableSRW(PCONDITION_VARIABLE pConditionVariable,PSRWLOCK pSRWLock,DWORD dwMilliseconds,ULONG Flags);
参数 pConditionVariable 指向一个已初始化的条件变量,调用线程正在等待该条件变量。
第二个参数是一个指向关键段或者 SRWLock 的指针,该关键段或 SRWLock 用来同步对共享资源的访问。
参数 dwMilliseconds 表示我们希望线程花多少时间(可以设成INFINITE)来等待条件变量被触发。
第二个函数中的 Flags 参数用来指定一旦条件变量被触发,我们希望线程以何种方式来得到锁:对写入者线程来说,应该传入0,表示希望独占对资源的访问;对读取者线程来说,应该传入 CONDITION_VARIABLE_LOCKMODE_SHARED,表示希望共享对资源的访问。
当指定的时间用完的时候,如果条件变量尚未被触发,函数会返回 FALSE,否则函数会返回 TRUE。注意,当函数返回FALSE的时候,线程显然并没有得到锁或关键段。
当另一个线程检测到相应的条件已经满足的时候,比如存在一个元素可以让读取者线程读取,或者有足够的空间让写入者线程插入新的元素,它会调用 WakeConditionVariable 或 WakeAllConditionVariable,这样
阻塞在 Sleep* 函数中的线程就会被唤醒。
VOID WakeConditionVariable(PCONDITION_VARIABLE ConditionVariable);
当我们调用 WakeConditionVariable 的时候,会使一个在 SleepConditionVariable* 函数中等待同一个条件变量被触发的线程得到锁并返回。当这个线程释放同一个锁的时候,不会唤醒其他正在等待同一个条件变量的线程。
VOID WakeAllConditionVariable(PCONDITION_VARIABLE ConditionVariable);
当调用 WakeAllConditionVariable 的时候,会使一个或几个在 SleepConditionVariable* 函数中等待这个条件变量被触发的线程得到对资源的访问权并返回。
唤醒多个线程是可以的,因为我们确信如果我们请求独占对资源的访问,那么同一时刻必定只有一个写入者线程得到锁,如果我们传给 Flag 参数的是 CONDITION_VARIABLE_LOCKMODE_SHARED,那么在同一时刻可以允许多个读取者线程得到锁。总结:
如果希望应用程序中得到最佳性能,那么首先应该尝试不要共享数据,然后依次使用 volatile 读取,volatile 写入,Interlocked API,SRWLock 以及关键段。当且仅当所有这些都不能满足要求的时候,再使用内核对象。