前言
前一篇学习了事件(Event)对象,线程在进入临界区之前会通过调用WaitForSingleObject或者WaitForMultipleObjects来判断当前的事件对象是否有信号(SignalState>0),只有当事件对象有信号时,才可以进入临界区。 需要说明的是,这里的临界区指的是广义上的临界区,即只允许一个线程进入直到退出的一段代码,不单指用EnterCriticalSection()和LeaveCriticalSection()而形成的临界区。
通过对Event对象相关函数的分析,我们发现,Event对象的SignalState值只有两种可能:
- 值为1:
- CreateEvent函数初始化Event时,第3个参数值为TRUE。
- 调用SetEvent函数设置Event对象为有信号。
- 值为0:
- WaitForSingleObject/WaitForMultipleObjects
- ResetEvent
事件的应用场景
修改全局变量
在对事件对象的相关知识有所掌握后,就轮到对其应用场景的学习。事件可以运用在 “当多个线程想要对同一个全局变量作修改时” 的情景,此时可以通过事件(例如WaitForSingleObject+SetEvent)形成的临界区,完成对线程进出临界区的控制,以保证同一时间只有一个线程可以修改全局变量。
生产者与消费者问题
生成者与消费者问题是经典的线程同步问题,需要解决的问题是在资源不对等的情况下,该如何确保线程同步。
如图,有1个生产者线程,每回合可以生产出3点资源,此时有5个消费者线程。需要保证在同一时间不会有2个线程消费同1个资源。
在这种情况下,使用事件来控制线程的同步就相当的困难,效率也相对较低:
- 若使用事件同步对象(Type=1),由于事件对象的SignalState的值只能为0或者1,因此同一时间只有一个消费者线程可以获得资源,此时效率是极其低下的。
- 若使用通知类型对象(Type=0),通知类型对象唤醒的线程,在进入KeWaitForSingleObject循环后,不会修改SignalState的值,但是此时仅有3点资源,唤醒5个线程,显然也会造成资源的浪费。
综上,在解决的生成者与消费者问题时,使用事件对象来处理,效率明显不够高。需要有另一种形式的同步对象,也就是本篇将要介绍的信号量。
信号量的应用场景
信号量和事件大体类似,不同的是,相较于事件对象同一时间仅允许一个线程进入临界区,信号量则允许多个线程同时进入临界区。
因此信号量可以应用在生成者与消费者问题上。当消费者线程的数量多于可被消耗的资源时,允许和资源数量相同的线程进入临界区,即可使得效率最大化。
信号量的创建与设置
再次回顾KeWaitForSingleObject的关键循环
之前已重复多次,不同类型的等待对象。区别在于对是否符合激活条件的判断,以及对SignalState值的修改。所以从这两个角度入手,会更容易理解信号量。
信号量的创建
下面将信号量创建API,信号量结构体,信号量第一个成员_DISPATCHER_HEADER放在一起看,会比较清晰。
- 首先是CreateSemaphore函数,比较关键的是第二个和第三个参数。
- 调用CreateSemaphore函数创建出信号量,也就是_KSEMAPHORE这个结构体。该结构体比Event对象多了一个Limit字段,该字段由CreateSemaphore的第三个参数IMaximumCount决定,用来设置最多允许多少线程同时进入临界区。
- _DISPATCHER_HEADER是每个可等待对象都拥有的成员,其中信号量类型为5(Type=5),SignalState的值由CreateSemaphore的第二个参数IInitialCount决定。
信号量的设置
之前学习的Event对象,它的SignalState由CreateEvent第三个参数决定,也可以通过SetEvent设置信号。
信号量的SignalState由CreateSemaphore第二个参数IInitialCount决定,也可以通过ReleaseSemaphore设置信号。
根据分析ReleaseSemaphore函数,其执行流程如上图所示,最终会调用内核的KeReleaseSemaphore函数,该函数主要作用也和SetEvent(Type=0)类似,区别也是在于对SignalState的修改上:
- SetEvent:将SignalState的值置1。
- ReleaseSemaphore:设置SignalState = SignalState + N(传入的参数)
ReleaseSemaphore函数,在解决生产者消费者问题时就更有效率,它可以根据生产出来的资源设置相应的信号。
KeWaitForSingleObject
同样,KeWaitForSingleObject这个函数也是必不可少的,几个主要的可等待对象都要经过它的循环,信号量的部分在上一期顺带提到了,这里可以直接拿来用。
这里直接定位到粉色方框部分,只有当Type值为5时,也就是信号量对象时,才会走这里。这里对SignalState值的修改方式是减1,和事件对象不一样。事件对象是直接将SignalState设置为0,信号量则是减1。这样信号量就可以精准控制进入临界区线程的数量,在解决例如生产者与消费者问题时更有效率。
参考资料
参考教程:
- 海哥中级预习班课程
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83449347 (CSDN-My classmates学习笔记)
- https://blog.csdn.net/qq_41988448/article/details/104895544 (CSDN-lzyddf学习笔记)