(5)用户态线程同步

最后更新于:2022-04-01 21:44:51

在多线程环境下,线程同步是不可避免的话题。Windows环境下的线程同步分为:用户态同步 与 内核态同步。 下面我们先了解用户态同步的一些方法。 - 使用Interlocked系列函数。简单易用的函数。 - 关键段。用来对关键资源实现独享访问。 - Slim读写锁。灵活的进行写独享读共享操作。 - 条件变量。当线程要进行较为复杂的条件进行同步时,可以实现。 Interlocked系列函数。 Windows提供了Interlocked系列函数,用来原子的对数据进行增减或交换。如自旋转锁,就可以通过InterlockedExchange函数实现。 ~~~ BOOL g_fResourceInUse = FALSE; void Func1() { // InterlockedExchange会一直原子设置g_fResourceInUse为TRUE,同时返回g_fResourceInUse // 上一次的值。当g_fResourceInUse为初始值FALSE或被另一线程设置为FALSE后,该循环才会结束。 // 以此自循环(自旋锁)方式来实现对资源的独占访问。 while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE) Sleep(0); // Access the resource, do something ... // Do not need the resource anymore, release it InterlockedExchange(&g_fResourceInUse,FALSE) } ~~~ 注意,这种循环方式会占用CPU大量时间,不建议在单CPU机器上运行。(可以用关键段代替或用C++11标准中的atom系列函数代替) 关键段 上面使用自旋锁的方式进行同步显然是低效的。因为等待线程依然处于可调度状态,仍然会占用CPU时间。Windows提供了一系列函数,让线程同步。这一系列函数保证了在线程获得想要的资源之前,不被CPU调度,直到其要求的资源可被线程访问为止。关键段就是其一,其实关键段的实现是通过事件内核对象的。 运用关键段五个步骤: 1、声明一个可以被多个线程访问到其地址的关键段变量。 2、在使用关键段前,调用InitializeCriticalSection函数初始化关键段。 2、在进入资源前调用EnterCriticalSection,请求进入关键段(若进入不了,则线程等待) 3、在离开资源时,调用LeaveCriticalSection,离开关键段。 若确定了关键段已经不被任何线程再使用,则要销毁关键段对象。 4、在不再使用关键段时,调用DeleteCriticalSection销毁关键段。 ~~~ CRITICAL_SECTION g_cs; int g_sum = 0; //初始化关键段,注意不要多次初始化,否则后果是未定义的
InitializeCriticalSection(&g_cs);
~~~
  
~~~
void ThreadFunc1()  
{  
     
   EnterCriticalSection(&g_cs);  
   g_sum++;  
   LeaveCriticalSection(&g_cs);  
}  
  
void ThreadFunc2()  
{  
     
   EnterCriticalSection(&g_cs);  
   g_sum++;  
   LeaveCriticalSection(&g_cs);  
}  
...  
...  
// 不再使用critical section,显示销毁  
DeleteCriticalSection(&g_cs);  

~~~

关键段最容易忘记
~~~
LeaveCriticalSection
~~~
,这时候可以用RAII技巧来进行简单的封装。

关于关键段的细节

1、若一个线程已经成功进入关键段,则可以多次调用EnterCriticalSection,相应的,要调用多次LeaveCriticalSection来离开临界区。

2、对于跨进程的线程同步,可以使用mutex对象。

  

可以使用TryEnterCriticalSection进入关键段,他不会使线程进入等待,而是返回布尔值表示是否获得了关键段。对于返回TRUE,需要调用LeaveCriticalSection。

  

关键段与旋转锁

当线程由于得不到关键段而进入等待状态时,会进行用户态和内核态切换,这会占用大量的CPU时间。在多处理器的环境下,可能的一种情况是,用户/内核态的切换还未结束,占用关键段的线程可能已经释放了关键段。

在多处理器的情况下,可以使用

[**InitializeCriticalSectionAndSpinCount**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms683476%28v=vs.85%29.aspx)

函数来初始化关键段。其函数原型如下

~~~
BOOL WINAPI InitializeCriticalSectionAndSpinCount(
  _Out_  LPCRITICAL_SECTION lpCriticalSection,
  _In_   DWORD dwSpinCount
);
~~~
  
其中参数dwSpinCout用来设置旋转锁循环次数。[**SetCriticalSectionSpinCount**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686197%28v=vs.85%29.aspx) 可以重设自旋转锁次数。

该函数会在进入内核态前,旋转设置的循环次数来获取关键段。若在旋转锁阶段获取关键段,则不会进入内核态。

注意,在单CPU模式下,dwSpinCout是被忽略的,总是为0。因为在单CPU下,

~~~
InitializeCriticalSectionAndSpinCount
~~~
是没有意义的:CPU在旋转锁阶段被线程占用,其他线程根本没有时机来释放关键段。但我们仍可以这样初始化关键段,以应对未来可能的多CPU环境。

  

Slim读写锁

一般的,对于线程的同步,读是可以共享的,而写则是互斥的。因此Windows提供了读写锁机制。

与关键段类似,在使用读写锁之前,要调用[**InitializeSRWLock**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms683483%28v=vs.85%29.aspx)函数初始化读写锁。

利用读写锁要分清读者和写者。

读写锁使用步骤

1、声明SRWLOCK对象。

2、用InitializeSRWLock函数初始化SRWLOCK对象。

3.1、对于读者,调用

[**AcquireSRWLockShared**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681934%28v=vs.85%29.aspx)

[**ReleaseSRWLockShared**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685080%28v=vs.85%29.aspx)

以共享的方式获取,释放的读写锁。若该锁没被占用或被其他线程读,则立即获得锁,否则等待。

3.2、对于写者,调用

[**AcquireSRWLockExclusive**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681930%28v=vs.85%29.aspx)

[**ReleaseSRWLockExclusive**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685076%28v=vs.85%29.aspx)

以独占的方式获取,释放的读写锁。若该锁未被占用,则立即获得锁,否则等待。

Slim与关键段的对比

Slim锁与关键段主要有以下两点区别

1、Slim锁不能够递归获取,即当一个线程Acquire并获得Slim锁之后,不能够再次Acquire同一把锁。

2、不存在TryEnter类似函数获取Slim锁。

3、Slim锁不用显示销毁,系统会自动释放。

4、总体上说,Slim锁的效率优于关键段。

  

多种同步方法的效率对比

![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/82c440c7466c6449356a9b8660ba37f6_617x136.jpg)

  

条件变量同步

有时候需要线程原子方式释放获得的锁同时阻塞自身,直到某一条件成立为止。这时候可以通过条件变量进行同步。

等待条件变量函数。当条件被满足,线程被唤醒后,会自动得到锁。

[**SleepConditionVariableCS**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686301%28v=vs.85%29.aspx)

[**SleepConditionVariableSRW**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686304%28v=vs.85%29.aspx)

唤醒等待条件的线程函数

[**WakeAllConditionVariable**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687076%28v=vs.85%29.aspx)

[**WakeConditionVariable**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687080%28v=vs.85%29.aspx)
                    
';