八 用户模式下的线程同步


        当所有线程都独立运行,而不需要相互通信时,系统达到最佳状态,但很少有线程能独立运行。一下两种情况,线程间需要通信:

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

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


8.1 原子访问:互锁函数族InterLocked


线程同步在很大程度上与原子访问有关,所谓原子访问: 指线程在访问某共享资源时,不允许其他任何线程在同时访问这一资源。

例如:定义全局变量 int  g_x=0;然后在两个线程中对该变量进行++g_x操作,由于线程的抢占性,因此其结果是g_x的值可能为1,也可能为2,是无法确定的。 而原子访问能保证一个线程在对某一资源访问不被打断(是指在该线程在完成对资源的访问前,不可能有其他的线程访问该资源,确保每次操作都能完整进行,并不是指该线程在对资源的访问期间整个时间连续的。同样进行线程的调度。) InterLocked系列函数正是确保这样的行为。

如函数: LONG InterlockedExchange(pLONG , LONG value); //把第一个参数所指地址的值以原子方式替换为第二个参数的值,并返回原值。


8.2  高速缓存行


当CPU从内存中读取一个字节时,实际上并不是读取回一个字节,而是读取回一个高速缓存行(可能包含32、64、128字节,取决于CPU)。一般应用程序都需要访问相邻的字节,如果所有的字节都存在高速缓存行中,每次就不用都从内存中读取,从而提高性能。  

但是在多处理器环境中,高速缓存行使得对内存的更新变得困难。由于多个CPU可能同时前后多一个字节进行读取到各自高速缓存行,并各自修改其内容,因此原来内存中的内容得不到及时的更新。当然在设计是会考虑这些问题: 例如当一个CPU修该了其高速缓存行的字节内容时,其他的CPU会得到通知,并将其高速缓存行中的内容作废,从新从内存中读取,当然在此之前,修改了字节的线程要及时把新的内容写入到原地址以供其他线程读取。  这一过程会损伤多处理器的性能。

这也意味着,我们应该根据高速缓存行的大小来把应用程序的数据组织在一起,并将数据与缓存行的的大小对齐。 此外应把只读数据与读写数据分开存放。

注:1、只要始终只让一个线程访问数据(函数参数与局部变量是最简单的方式)

          2、或始终只让一个CPU访问数据(使用线程的关联性实现)

只要确保以上二者之一,就能完全避免高速缓存行的问题。



8.3 高级线程同步


Interlocked函数只能对简单的32位或64位数据进行原子访问,而不能对复杂数据结构操作:既能等待资源的访问权,又不会浪费CPU时间。(旋转锁通过while循环来等待资源的访问权,浪费CPU时间)。

当线程要访问一个共享资源或者等待“特殊事件”的通知时,线程将调用一个系统函数,并把需要的东西以参数传入。如果系统检测到资源已可访问或者事件已发生,则该函数立即返回,线程仍然保持可调度状态;反之,则将该线程切换到等待状态,而不可调度,从而避免浪费CPU时间。当线程等待时,系统会从当其代理,当资源可用或者事件发生时将线程唤醒为可调度状态——线程的执行与特殊事件同步。

实际情况是系统的大多数线程都处于等待,当系统检测到多有的线程都处于等待几分钟后,系统的电源管理介入。

注:在变量前加 volatile限定符,告诉编译器不对该变量进行任何的优化,每次都从内存(而不是CPU寄存器)读取数据,以便当其值更新时能及时读得新值。


8.4  关键段:critical section

关键段是一小段代码,在它执行某资源的访问时,独占其访问权,以保证“原子操作”——没有其他任何资源同时访问该资源。

例:

<span style="background-color: rgb(255, 255, 255);"><span style="color:#330033;">int g_n=0;//共享资源
CRITICAL_SECTION g_cs;//用于标示一个共享资源的结构体
FirstThread(PVOID pParam)
{
  //初始化结构
   EnterCriticalSection(&g_cs);//申请进入访问g_cs访问的资源,并标识该资源已被占用。
   ++g_n;//访问资源 原子操作
   LeaveCriticalSection(&g_cs);//结束对资源的访问,并释放占用权</span><span style="color:#6600cc;">
}</span></span>


另外一个线程特与而进行同样的访问操作。   任何要访问资源的代码都必须在 EnterCriticalSection和LeaveCriticalSection之间。即每个线程在访问资源之前都要申请并等待资源的使用权,然后独占访问,访问结束后主动交还使用权。

关键段在内部使用了Interlocked,因此其速度极快,缺点是无法在多个进程间对线程进行同步。


8.4.1  关键段:细节

</pre><pre name="code" class="cpp"><span style="white-space:pre">	</span>注:在对调用EnterCriticalSection之前应对Critical_Section结构初始化.。

可用BOOL TryEnterSection(critical_section Pcs)代替EnterSection(critical_section Pcs)。该函数用返回值表示调用线程是否获得准许访问资源,若不能访问资源,返回false值,然后继续执行其他代码,而不会让线程进入等待状态。



8.4.2 关键段和旋转锁

实际为了关键段效率,Microsoft在关键段中合并了旋转锁。当调用EnterCriticalSection时,会用一个旋转锁不段的循环等待资源可访问,在尝试一段时间获得访问权,只有当尝试失败是才会切换到内核模式进入等待状态。


8.4.3  关键段和错误处理


8.5  Slim读/写锁


  SRWLock和关键段的目的相同:对一资源进行保护,不让其他线程同时访问资源。但SRWlock区分对资源的读写,让所有的读取者线程同时访问资源,而写入者线程独占访问权。从而对那些只读资源的线程提高了吞吐量和可伸缩性。

总之要使线程达到最佳性能:(性能依次递减)

不要共享数据——>volatile读/写——>ItnterLockedAPI——>SWRLock——>关键段——>内核对象。





8.6  条件变量

在以原子方式访问资源时,有时希望线程将锁释放并将自己阻塞,直到某一条件满足为之。让线程调用函数SleepConditionVariableSRW/CS(pCondition ,、、、)参数指向一个已初始化的条件变量,调用线程等待该条件,可以设置等待条件被触发的时间,当时间用完还没有被触发时返回FALSE,线程没有获得锁或关键段。当另一线程测得条件一满足时可调用WakeConditionVariable或WakeAllConditionVariable唤醒阻塞在Sleep*的线程。

8.6.1  Queue示例程序

8.6.2 在停止线程时的死锁问题

8.6.3 一些有用的窍门和技巧

1、以原子方式操作一组对象时使用一个锁

2、同时访问多个逻辑资源时,在代码的任何地方都必须使用完全相同的访问顺序来获得资源的锁。

3、不要长时间占用锁,以免放其他线程长时间等待进入等待状态,影响到程序性能。

在只读取数据时,可用资源的临时拷贝代替,以减少对共享资源的占用时间。









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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值