windows系统多线程同步机制原理总结

windows系统多线程同步机制原理总结

同步问题是开发过程中遇到的重要问题之一。同步是要保证在并发执行的环境中各个控制流可以有序地执行,包括对于资源的共享或互斥访问,以及代码功能的逻辑顺序。
为了保证多线程间的同步,Windows操作系统提供了一系列的机制:事件、互斥体、信号量等等。本文主要基于对《Windows内核原理与实现》一书相关章节的整理并结合自己的理解介绍同步机制的大概实现原理,有任何不妥的地方,希望大家能够不吝指出。

Windows线程调度

我们先来简单的说以下Windows的线程调度,这是理解后续东西的基础。Windows的调度算法是一个抢占式的、支持多处理器的优先级调度算法。这种调度算法将处理器的时间分成一个个的时间片段,每个时间片段分给不同的线程去执行线程中的指令,直到时间片用完。显然,这样做使得处理器的执行不是按照一个事先知道的顺序依次进行;操作系统在调度线程时需要一个时钟中断来获得对处理器的控制权,从而引导处理器去执行操作系统想要它执行的目标线程的指令。

中断

说到Windows的线程调度是通过中断来切换线程的,那么到底什么是中断呢?我们这样想:处理器在执行指令时肯定是顺序执行指令流,那么它怎么处理一些事先无法预知、运行时才发生的事件呢?比如用户某一时刻突然敲下键盘。一种办法是处理器足够频繁地去挨个检查所有可能发生的事件是否发生了,这种方法显然很笨;另一种方法就是使用中断。
中断其实包含硬件中断和软件中断,说说硬件中断(软件中断其实是模拟的硬件中断)。硬件中断是外部设备在需要通知处理器去做一件事的时候向处理器的特定管脚(NMI和INTR)发送数据,处理器每执行完一条指令都会去检查这个管脚的状态,看看是否有中断发生。通过中断机制,处理器就不需要去挨个设备的检查了,只需要统一的检查是否发生了中断。那么,处理器在得知发生中断后,又怎么知道发生了什么中断?以及怎么去处理中断呢?
原来,每个中断都有一个中断编号,也称为中断向量。外部设备在触发处理器中断时会发生相应的中断向量。除此之外,操作系统中维护一个中断描述符表(IDT),这个表将每个中断向量与中断服务例程(用来处理该中断的一段程序)关联起来。处理器根据中断向量查询IDT,从而找到中断服务例程的地址,去执行中断服务例程,完成对中断的响应。

同步机制的实现

现在我们明白了操作系统是怎么调度线程的了,这是这种调度方式使得线程的同步有了很多不同的方式。那怎么实现呢?

1. 不依赖于线程调度的同步机制

第一种做法是对中断下手。需要申明,这种方法只对单处理器有效。单处理器情况下,导致资源争用的罪魁祸首是线程调度,它使得一个处理器分时并行地干几件事,就好像有多个处理器一样。那不让线程调度不就行了?对!前面说到了,线程调度是依靠中断实现的,那就从中断下手。处理器在处理中断时,并不是什么中断都处理,它会判断当前条件下是否需要处理某一中断。而Windows提供了一套中断级别定义方案,叫做中断请求级别(IRQL)。它使用0~31来表示优先级,数值越大,优先级越高;处理器任何时候都运行在一个级别上,且只能被更高级别的中断打断。感兴趣的同学可以去查查IRQL到底包含哪些级别,本文只说其中一个级别——DISPATCH_LEVEL,从名称中可以大致猜到这是线程调度时所在的级别。因此,如果在访问需要同步的资源之前,将处理器的运行级别提高到DISPATCH_LEVEL或更高的IRQL,这时操作系统就不会调度线程了,对单处理器而言就不存在其他线程同时访问资源的情况,也就实现了目标。

下面划重点:自旋锁我们都知道。它的实现就利用了这种方式,因此,线程在自旋等待时不会有线程切换。也正是因为不会有线程切换,省去了切换的耗时,因此它很适合预期等待时间很短的情况使用。

2. 基于线程调度的同步机制

第二种方法我们不去干涉线程调度。那么要想让不同线程能够协调起来,我们需要一个全局的东西去协调它们。这个东西就是同步对象,也称为分发器对象。无论是互斥体还是信号量还是其他什么同步方式,它们都需要针对一个确定的分发器对象。一个分发器对象有基本的两个状态:有信号和无信号状态,分发器对象初始化时为有信号状态。当一个线程执行某种同步方式时,它去查看指定分发器对象是否有信号,若有信号,该线程继续执行,同时该分发器对象的状态变为无信号;再有别的线程去查看该分发器对象时,发现状态为无信号,此时该线程进入等待状态(睡眠),操作系统的线程调度将不再调度该线程。这就实现了只能一个线程访问同一资源。

那么,当第一个线程执行完了后,分发器对象的状态重新设定为有信号后,其他在等待该分发器对象的线程又怎么知道呢?这就涉及到线程对象和分发器对象的数据结构。在线程对象和分发器对象中都有同样的一个数据成员:一个指向某一链表头节点的指针,而这个链表是一个等待块对象链表。每个等待块对象都记录了哪个线程在等待哪个对象。是的,等待块对象会同时加入到两个链表中:一个是线程对象中的链表,一个是分发器对象中的链表。
现在我们回到之前的情形,当别的线程去查看该分发器对象时,发现状态为无信号,此时实例化一个等待块对象,它记录了当前线程在等当前分发器对象,这个等待块对象都添加到了线程对象和分发器对象的链表中;当分发器对象重新变为有信号后,它会去唤醒等待块链表中记录的线程对象,此时等待的线程的状态变成延迟的就绪状态,等待操作系统线程调度器来调度。

分发器对象有很多种,具体可分为:事件、突变体、信号量、进程、线程、队列、门、定时器。有些名字听起来是不是很熟悉?是的,它们都对应着不同的同步方式:比如事件就对应同步中的事件、突变体对应互斥体…这些分发器对象在数据结构上分为两部分:对象头部和对象体。不同分发器的对象头部是一样的(对象头部的第三个数据正是等待块链表的头指针),对象体因功能不同而不同(类似于面向对象里的多态性)。对象体不同,正是与之对应的同步方式功能有所不同的本质原因。比如说突变体对象的对象体中有一个所有者概念,用于表示当前拥有该突变体对象的线程(我认为这就是互斥锁能作为递归锁的原因)。

需要说一下,这些同步方式都需要处理器提供支持。如果处理器不提供原子运算的功能,那么什么事件、信号量等等都是空谈,因为至少在对分发器对象进行判断的时候必须要保证其过程是原子操作。现代处理器会提供一些原子运算指令,比如在Inter x86指令体系中,有些运算指令加上lock前缀就可以保证其原子性,lock前缀指令使用的两个条件:

  1. 指令的目标操作数必须一个是内存操作数;
  2. 仅适用于ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD、XCHG。

最后总结一下,我们常见的线程同步方式,如事件、互斥体等,它们的实现都是依靠于分发器对象。操作系统通过查看分发器对象的状态,判断一个线程是等待还是执行。

你可能感兴趣的:(操作系统)