用户模式下的线程同步

在以下两种基本情况下,线程之间需要相互通信

1.需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性

2.一个线程需要通知其他线程某项任务已经完成。

原子访问相关的内容就直接略过了,因为感觉实际使用的过程中并不多。

下面直接开始说一下关键段,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以原子方式来对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。

一般是EnterCriticalSection和LeaveCriticalScetion配对使用,需要先创建一个CRITICAL_SECTION结构。

这结构一般都是作为全局变量来使用,也可以作为局部变量来分配或者堆中,另外作为类的一个私有字段还分配也是很常见的。

在使用CRITICAL_SECTION的时候,只有两个必要条件,第一个是所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址,第二个是任何线程试图访问被保护的资源之前,必须对CRITICAL_SECTION结构的内部成员进行初始化。

下面的函数用来进行初始化

void InitializerCriticalSection(PCRITICAL_SECTION pcs);

当线程不在需要访问共享资源的时候,应该调用下面的函数来清理结构

void deleteCriticalSection(PCRITICAL_SECTION pcs);

EnterCriticalSection会检查结构体中的成员变量,这些变量表示是否有线程正在访问资源,以及哪个线程正在访问资源。EnterCriticalSection会执行下面的测试:

1.如果没有线程正在访问资源,那么EnterCriticalSection会更新成员变量,以表示调用线程已经获准对资源的访问,并立即访问,这样线程就可以继续执行

2.如果成员变量表示调用线程已经获准访问资源,那么EnterCriticalSection会更新变量,以表示调用线程被获准访问的次数,并立即放回,这样线程就可以继续执行。这样的情况非常少见,只有当线程调用LeaveCriticalSection之前连续调用EnterCriticalSection两次以上才会发生。

3.如果成员变量表示有一个(调用线程之外的其他)线程已经获准访问资源,那么EnterCriticalSection会使用一个事件内核对象来吧线程切换到等待状态。一旦当前正在访问资源的线程调用了LeaveCriticalSection,系统会自动更新结构的成员变量并将等待中的线程切换回可调度状态。

我们也可以用下面的函数来代替EnterCriticalSection

Bool TryEnterCriticalSection(PCRITICAL_SECTION pcs);

TryEnterCriticalSection从来不会让调用线程进入等待状态,他会通过返回值来表示调用线程是否获准访问资源,因此如果TryEnterCriticalSection发现资源正在被其他线程访问,那么它会返回false。每一个返回true的TryEnterCriticalSection调用必须有一个对应的LeaveCriticalSection。

当线程试图进入一个关键段,但这关键段正在被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式,这个切换的开销非常大。为了提高性能,可以将旋转锁合并到关键段中,因此当调用EnterCriticalSection的时候,他会用一个旋转锁不断的循环,尝试在一段时间内获取的对资源的访问权,只有当尝试失败的时候买线程才会切换到内核状态并进到等待状态。

为了在是因关键段的时候同时使用旋转锁,必须调用以下的函数来初始化关键段

Bool InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);

与InitializerCriticalSection相似,但是第二个参数是我们希望旋转锁循环的次数,我们也可以通过下面的函数来改变关键段的旋转次数

DWORD WINAPI SetCriticalSectionSpinCount(
  _Inout_ LPCRITICAL_SECTION lpCriticalSection,
  _In_    DWORD              dwSpinCount
);

用来保护进程对的关键段所使用的旋转次数大约是4000,这里作为参考。

SRWLock的目的和关键段系统,对一个资源进行保护,不让其他资源访问他。但是与关键段不同,SRWLock允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有的读取者线程在同一时刻访问共享资源应该是可行的,只有当写入者线程需要对资源进行更新的时候才需要同步,在这样子情况下,写入者线程应该独占对资源的访问权:任何其他线程。

首先我们需要分配一个SRWLOCK结构并用InitialSRWLock函数来对他进行初始化

VOID WINAPI InitializeSRWLock( _Out_  PSRWLOCK 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以及关键段。仅当这些都不能满足要求的时候,再使用内核对象。

Windows提供SleepConditionVariableCS或SleepConditionVariableSRW函数,等待条件变量。线程在等待该条件变量时,会以原子方式把锁释放并将自己阻塞,直到该条件变量被触发时为止。

Bool SleepConditionVariableCS(  
	PCONDITION_VARIABLE pConditionVariable,  
  	PCRITICAL_SECTION pCriticalSection,  
 	DWORD dwMilliseconds);  

Bool SleepConditionVariableSRW(  
  	PCONDITION_VARIABLE pConditionVariable,  
 	PSRWLOCK pSRWLock,  
 	DWORD dwMilliseconds  
  	ULONG Flags);

 pConditonVariable指向一个以初始化的条件变量,调用线程将等待该条件变量。第二个参数指向一个关键段或是SRWLock对象。该关键段或SRWLock用来同步对共享资源的访问。Flags指定一旦条件变量被触发,线程将以何种方式获得锁。对读取者线程来说应该传入CONDITION_VARIABLE_LOCKMODE_SHARED表示希望共享对资源的访问。对于写入者线程应该传入0,表示独占资源。

dwMilliseconds表示我们希望线程花多少时间来等待条件被触发。在指定的时间用完时,如果条件变量尚未被触发,函数返回false,否则为true。

当另一个线程检测到相应的条件已经满足时,比如存在一个元素可以让读取者线程读取。它会调用WakeConditionVariable或WakeAllConditionVariable,触发条件变量。这样调用Sleep*函数而阻塞在该条件变量的线程就会被唤醒。

Void WakeConditonVariable(  
   PCONDITION_VARIABLE ConditionVariable);  
Void WakeAllConditionVariable(  
   PCONDITION_VARIABLE ConditionVariable);
WakeConditionVariable会使SleepConditionVariable*等待的同一个条件变量被触发的线程得到锁并返回。当此线程释放这个锁的时候,不会唤醒其他正在等待此条件变量的线程。




  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值