windwos核心编程读书笔记5——线程(5)用户态线程同步

在多线程环境下,线程同步是不可避免的话题。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;
//初始化关键段,注意不要多次初始化,否则后果是未定义的
<pre name="code" class="cpp">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

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

BOOL WINAPI InitializeCriticalSectionAndSpinCount(
  _Out_  LPCRITICAL_SECTION lpCriticalSection,
  _In_   DWORD dwSpinCount
);

其中参数dwSpinCout用来设置旋转锁循环次数。 SetCriticalSectionSpinCount 可以重设自旋转锁次数。

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

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

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


Slim读写锁

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

与关键段类似,在使用读写锁之前,要调用InitializeSRWLock函数初始化读写锁。

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

读写锁使用步骤

1、声明SRWLOCK对象。

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

3.1、对于读者,调用

AcquireSRWLockShared

ReleaseSRWLockShared

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

3.2、对于写者,调用

AcquireSRWLockExclusive

ReleaseSRWLockExclusive

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

Slim与关键段的对比

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

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

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

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

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


多种同步方法的效率对比


条件变量同步

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

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

SleepConditionVariableCS

SleepConditionVariableSRW

唤醒等待条件的线程函数

WakeAllConditionVariable

WakeConditionVariable


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值