《Windows核心编程》——八 用户模式下的线程调度

前言

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

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

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

 

8.1 原子操作:InterLocked序列函数

    所谓原子操作,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。

    所有InterLocked序列函数会以原子方式来操作一个值。这类函数执行得极快,调用一次该序列函数通常只占用几个CPU周期(通常小于50),而且也不需要在用户模式和内核模式之间进行切换(这个切换通常需要占用1000个周期以上)。

    栈中的每个操作,比如入栈或出栈,必定是以原子方式进行的。

 

8.2 高速缓存行

    当CPU从内存中读取一个字节的时候,它并不只是从内存中取回一个字节,而是取回一个高速缓存行。高速缓存行存在的目的是为了提高性能。一般来说,应用程序会对一组相邻的字节进行操作。如果所有字节都在高速缓存中,那么CPU就不必访问内存总线,后者耗费的时间比前者耗费的时间要多得多。

    在多处理器环境中,高速缓存线使得对内存的更新变得更加困难。可以从下面的例子中体会到这一点。

8.3 高级线程同步

    旋转锁是通过轮循方式处理的。

    我们需要一种机制,它既能让线程等待共享资源的访问权,又不浪费CPU时间。

    如果无法取得对资源的访问权,或者特殊事件尚未发生,那么系统会将线程切换到等待状态,使线程变得不可调度,从而避免了让线程浪费CPU时间。当线程在等待的时候,系统会充当它的代理。系统会记住线程想要访问什么资源,当资源可供使用的时候,它会自动将线程唤醒——线程的执行与特殊事件是同步的。

    线程同步一个通用的规则是,我们既不应该使用旋转锁,也不应该进行轮询,而应该调用函数把线程切换到等待状态,直到线程想要访问的资源可供使用为止。这种方法可以保证可能低优先级的线程能够被调度。

    volatile限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。给一个结构加volatile限定符等于结构中所有的成员都加volatile限定符,这样可以确保任何一个成员始终都是从内存中读取的。如果传一个变量的地址给函数,那么函数必须从内存中读取它的值,编译器的优化程序不会对此产生影响。

 

8.4 关键段

    关键段(Critical Section)是一小段代码,它在执行之前需要独占对一些资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。

    忘记调用EnterCriticalSection和LeaveCriticalSection就好像未经许可就进入卫生间一样。线程强行进入并对资源进行操控。正如我们想象的那样,只要有一个线程有这种粗暴的行为,资源就会被破坏。当不能用InterLocked函数解决同步问题的时候,我们应该试一试关键段。关键段的最大好处在于它们非常容易使用,而且它们在内部也使用了InterLocked函数,因此执行速度非常块。关键段的最大缺点在于它们无法用来在多个进程之间对线程进行同步。

8.4.1 关键段:细节(Winbase.h中有CriticalSection的定义)(线程所有权问题,一个线程可以多次获取关键段的访问权)(只能用于互斥不能用于同步

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

    当知道线程将不再需要访问共享资源的时候,应该调用DeleteCriticalSection函数来清理CriticalSection结构。在对共享资源进行访问之前,必须调用EnterCriticalSection来获取访问资源的权利。EnterCriticalSection会执行下面的测试。

LeaveCriticalSection,系统会自动更新CriticalSection的成员变量并将等待中的线程切换回可调度状态。

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUGDebugInfo;//调试用的
    LONGLockCount; //初始化为-1,n表示有n个线程在等待。
    LONGRecursionCount; //表示该关键段的拥有线程对此资源获得关键段次数,初为0。
    HANDLEOwningThread; // from the thread's ClientId->UniqueThread,即拥有该关键段的线程句柄
    HANDLELockSemaphore;//实际上是一个自复位事件。
    DWORDSpinCount; //旋转锁的设置,单CPU下忽略


} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

由这个结构可以知道关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。事实上它会用第四个参数OwningThread来记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。
因此可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。

    如果EnterCriticalSection把一个线程切换到等待状态,那么在很长一段时间内系统可能不会去调度这个线程。事实上,在一个编写得非常糟糕的应用程序中,系统可能再也不会给这个线程调度CPU时间了。如果发生这种情况,我们说线程在挨饿。实际情况是,等待关键段线程是绝对不会挨饿的。对EnterCriticalSection的调用最终会超时并引发异常。

    可以使用TryEnterCriticalSection来替代EnterCriticalSection,前者从来不会让调用线程进入等待状态。它会通过返回值来表示调用线程是否获准访问资源。因此,如果TryEnterCriticalSection发现资源正在被其他线程访问,那么它会返回false。其他情况下,它会返回true。当返回true时,那么CriticalSection的成员变量已经更新过了,以表示该线程正在访问资源,因此每个返回值为true的TryEnterCriticalSection调用必须有一个对应的LeaveCriticalSection。在代码完成对共享资源的访问后,也应该调用LeaveCriticalSection。

    LeaveCriticalSection会检查结构内部的成员变量并将计数器减1,该计数器用来表示调用线程获准访问共享资源的次数。如果计时器大于0,LeaveCriticalSection会直接返回,不执行任何其他操作。如果计时器变成了0,LeaveCriticalSection会更新成员变量,以表示没有任何线程正在访问被保护的资源。它同时会检查有没有其他线程调用了EnterCriticalSection而处于等待状态。如果至少有一个线程处于等待状态,那么函数会更新成员变量,把其中一个处于等待状态的线程切换回调度状态。与EnterCriticalSection相似,LeaveCriticalSection会以原子方式执行所有的测试和更新操作。但是LeaveCriticalSection从来不会把线程切换到等待状态,它总是立即返回。

 

8.4.2 关键段和旋转锁(用户应该在使用关键段的时候同时使用旋转锁)

等待是处于内核模式。

8.4.3 关键段和错误处理

    InitializeCriticalSection可能会失败,失败的原因是它会分配一块内存,如果内存分配失败,那么该函数会抛出STATUS_NO_MEMORY异常。可以使用InitializeCriticalSectionAndSpinCount函数来更容易低发现这个问题,这个函数也会为调试信息分配一块内存,但如果内存分配不成功,它会返回false。

8.5 Slim读/写锁

    SRWLock的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。但是不同点在于SRWLock允许我们区分读取者线程和写入者线程。可以让所有的读取者线程在同一时刻访问共享资源,这是因为仅仅读取资源的值并不存在破坏数据的风险。只有当写入者线程想要对资源进行更新的时候才需要同步。在这种情况下,写入者线程应该独占对资源的访问权:任何其他线程,无论是读取者线程还是写入者线程,都不允许访问资源。这就是SRWLock的全部功能。

    首先我们需要分配一个SRWLock结构并用InitializeSRWLock函数对于进行初始化。

    其次写入者线程就可以调用AcquireSRWLockExclusive来尝试获得对保护的资源的独占访问权;读取者线程调用AcquireSRWLockShared。

    最后完成对资源的更新后,应该调用ReleaseSRWLockExclusive来解除对资源的锁定;读取者线程调用ReleaseSRWLockShared。

    不存在用来删除或销毁SRWLock的函数,系统会自动执行清理工作。

 

    与关键段相比,SRWLock缺乏下面两个特性。

    总结一下,如果希望在应用程序中得到最佳性能,那么首先应该尝试不要共享数据,然后依次使用volatile读取、volatile写入、InterLocked API、SRWLock以及关键段。当且仅当所有这些都不满足要求的情况下,在使用内核对象。

 

8.6 条件变量(生产者消费者模型)

    条件变量总是和锁一起使用:既可以是关键段,也可以是SRWLock。

    条件变量是等待的对象,锁是用来同步对资源的访问。可以理解为当指定的条件变量被触发,线程可以以某种方式得到锁。

 

    一些有用的窍门和技巧:

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

    2.同时访问多个资源,为了防止死锁,我们必须在代码中的任何地方以完全相同的顺序来获得资源的锁。注意在调用LeaveCriticalSection的时候顺序无关紧要,这是因为该函数从来不会让线程进入等待状态。

    3.不要长时间占用锁(如果一个锁被长时间占用,那么其他线程可能会进入等待状态,这会影响应用程序的性能)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值