并发性:互斥与同步1
并发原理
进程和线程的管理是操作系统操作系统需要管理这么多进程和线程(这里讨论进程,毕竟是进程是资源的所有者),每个进程需要占有一定的资源,操作系统如何才能协调好整个系统?
并发是现代计算机几乎都有的特点:
- 多道程序设计技术:单处理器中的多个进程并发
- 多处理器技术:多处理器系统中的多个进程并发
- 分布式处理器技术:多台分布式计算机中的多个进程并发
并发的基本需求是加强进程的互斥能力,什么叫互斥?简单来说,一个进程如果当前拥有一个资源,它就具有排斥所有其他进程的能力。
基本概念
首先首先,需要分清楚,什么叫并发,什么叫并行?
- 并发:许多进程通过操作系统轮流使用处理器,通过操作系统的调度让程序看起来是同时运行的。
- 并行:在多处理器系统中,几个进程分别在不同的处理器上同时运行。
我们考虑并发的互斥而不考虑并行的互斥,为什么呢?并行是几个进程同时执行,他们一般不会共享资源,但是如果他们共享了内存这种的缓存区域等等,也是需要考虑互斥的,只不过那超出了一般并行程序的设计范围。并行之间一般不会考虑互斥同步,因为他们实现互斥可能会导致性能瓶颈消耗资源,所以一般都是尽量避免竞争而不是考虑互斥。
接下来是一些枯燥但重要的概念,可以直接查表:
概念 | 解释 |
---|---|
原子操作 | 一个函数或者动作,内部实现对外不可见,无法被中断,要不他整个执行完,要不不执行 |
临界区 | 一段代码,将访问共享资源的代码,如果这个资源被占用,它将不能执行 |
死锁 | 两个或者两个以上的进程等待其他进程释放资源而无法继续运行 |
活锁 | 两个或者两个以上的进程一直因为其他进程改变自己的状态,但是不干有用的事情 |
互斥 | 如果有一个进程已经进入临界区,其他进程将无法进入临界区访问此资源 |
竞争条件 | 程序运行的结果依赖两个进程执行的速度(对资源的竞争),而无法确定 |
饥饿 | 两个或者两个以上的进程本来可以直接运行,但是一直被调度忽视,无法执行 |
进程同步 | 多个进程之间协调和控制各自的执行顺序,以避免因相互干扰而造成的数据不一致、进程死锁等问题 |
并发带来的问题
程序执行的结果依赖于进程执行的速度,即处理器执行的多个进程的指令的顺序(竞争条件)。操作系统不允许出现这种情况发生,而因为并发的要求,操作系统需要关注以下这些问题:
- 操作系统能跟踪每个进程,通过进程控制块就行(进程控制块储存进程的各种信息,操作系统通过这些信息跟踪进程)
- 操作系统要为每个进程分配和释放各种资源,(这涉及到进程的死锁控制)有些资源是不能共享给几个进程使用的(称之为临界资源),如下
- 处理器时间
- 内存
- IO设备
- 文件
- 操作系统需要隔离进程,保护每个进程不被其他进程的干扰和破坏(这涉及到文件,IO和内存的管理机制)
- 操作系统要确保每个进程的执行结果和执行速度无关(解决竞争,即实现互斥)
进程交互
根据进程之间知道对方是否存在的程度,对进程间的交互方式进行分类,可以分为以下三种:
感知程度 | 关系 | 进程间的影响 |
---|---|---|
不知道对方存在 | 竞争 | 进程的结果与另一个进程无关,完全不影响 |
间接知道对方存在(共享对象) | 通过共享合作 | 一个进程的结果可能却决于从另外一个进程取得的信息 |
直接知道对方存在(可以通信) | 通过通信合作 | 一个进程的结果可能却决于从另外一个进程取得的信息 |
实际上,两个进程间的关系远不止这么简单,举个简单的例子,两个合作的进程完全可以表现出竞争的关系。竞争和通过共享合作的进程需要互斥,但通过通信合作的进程不需要互斥,因为它们通过消息的发送和接收来实现进程间的同步,而不是共享临界资源。后续的消息传递会具体说到这种情况如何实现进程同步。
互斥的要求
简单的说,互斥就是进程在拥有临界资源后,有排斥其他进程进入临界区的能力,那具体来说?
- 互斥需要强制执行,不允许任何进程破例。
- 一个没有进入临界区的进程不允许干扰其他进程
- 不允许出现死锁或者饥饿
- 如果临界资源空闲,那一个需要进入临界区的进程应当可以立即进入
- 对进程的执行速度和处理器数量没有任何要求
- 一个进程不能永远拥有临界资源
这才是完整的互斥概念,那为了实现这些互斥的要求,计算机进行了何种设计呢?这里忽略软件层面的设计,从硬件角度和操作系统或程序语言角度讲。
硬件层面的互斥实现
中断禁用
让一个进程在临界区不能被中断,看似是一个好方法,但是代价很大:
- 执行效率明显降低,不能中断会带来系统的各种方面的卡顿
- 不能用于多处理器机器,因为一个进程的禁用中断,另外一个处理器上的进程依然可以访问临界资源,无法解决问题
专用机器指令
常见的比如比较和交换指令:通过一个测试值测定某个地址的值(就是那个确定是否被占用的变量),如果为这个地址为这个测试值,将一个新的值替换到那个地址,此时认为此临界区可以访问。否则,进入忙等待模式,即一个死循环,一直检测那个地址是否为测试值,直到临界资源可用。
注意:整个比较和交换指令都不允许被中断,确保它的原子性是实现互斥的前提。
还有一种机器指令:交换指令 。几乎所有处理器都有这个指令或者同原理指令,本质上都是一样的,即设定一个检测的值,如果每次进入临界区的时候改变它的值,别的进程进入临界区需要先检查这个值。
机器指令有很多优点:
- 适用于单处理器或者共享内存的多处理器上的任意数量的进程
- 简单,易于证明
- 可用于支持多个临界区,每个临界区一个变量定义就行
但是也有很多缺点:
- 使用忙等待。死循环一直小号处理器时间
- 可能饥饿。一个进程能否进入临界区取决于当占用的进程释放资源的时候,它是否是第一个试图进入临界区的,因此则有可能导致饥饿
- 可能死锁。如果一个进程使用了一个资源,但是处理器调度到另外一个优先级更高的进程,如果它也需要这个资源,但是处理器无法调度前者,将导致死锁
纯硬件果然不太行啊,那如果在操作系统引入某些机制呢?
操作系统或程序语言层面的互斥实现
其实操作系统可以使用很多并发机制:
机制 | 描述 |
---|---|
信号量 | 也称计数信号量(为了跟二元信号量区分)一个整数值,只可以进行三种操作,且都是原子操作:初始化,增加,递减。 |
二元信号量 | 类似信号量,但是为布尔值 |
互斥量 | 类似二元信号量,但是加锁和解锁都需要同一个进程执行 |
条件变量 | 数据类型,用于阻塞进程(线程),直到为真释放进程 |
管程 | 一种编程语言结构,一个封装了变量的类,访问它的变量只能通过这个管程,并且每次只能在一个进程中访问,同时可以有进程等待队列 |
事件标志 | 用作同步机制的内存字,一个事件一个字,一个进程只有它等待的所有事件标志为真,才可以执行,以此实现互斥 |
消息 | 两进程用通过信息交换同步 |
自旋锁 | 在死循环中访问一个字段,直到它为真才访问临界资源 |
一般来说,一个进程通过wait判断是否进入临界区,并且在使用外临界资源后通过signal释放临界资源。
下面将讨论三种并发机制,并进行稍详细的解释
信号量
计数信号量
计数信号量可以被初始化为非负值,以下对计数信号量各个概念进行解释:
- 这个数字代表什么?如果为0或者为正,代表当前可以有多少个进程可以访问这个资源。比如为1,它减两次才为负,所以现在可以有两个进程进入临界区。如果为负,代表当前有多少个进程被阻塞到这个资源上。
- wait操作:信号量减一,如果为非负,这个进程可以进入临界区,否则被阻塞,将此进程放在阻塞队列上。
- signal操作:信号量加一,如果为非负,代表有进程正阻塞在这个资源上,去阻塞队列挑一个执行吧。否则啥也不干
- 操作过程:进程试图进入临界区先wait一下,访问完毕操作完后,signal一下代表释放了这个资源
二元信号量
二元信号量能实现跟计数信号量一样的效果:
- 真和假代表什么?显而易见,代表这个资源能否访问
- wait操作:如果为假,阻塞这个进程,加入到阻塞队列上,如果为真,改为假,然后直接访问临界资源吧
- signal操作:检测当前的阻塞队列,如果为空,改信号量为真,如果不为空,不改变信号量,从队列中拿一个出来执行
- 操作过程:跟计数信号量类似,访问前wait,访问后signal
注意
随意的从队列中拿一个来执行显然不行,对此可以区分两种信号量:
- 强信号量:规定了怎么拿进程的信号量,一般为FIFO。可以防止饥饿,毕竟轮着拿嘛
- 弱信号量:没有规定。
实现
wait和signal都显而易见,必须是原子操作,这里称之为:原语。如何实现?
- 软件实现,有且只有一个进程控制信号量,这样会导致开销
- 硬件实现,如果只有一个处理器,禁用中断就可以了,wait和signal比较小,禁用影响不大;也可以通过忙等待的比较交换指令实现
管程
管程可以想象成一个类,它更好控制,并且它理论上可以锁住任何对象,它有以下特点:
- 局部变量只能由管程访问,任何外部过程都不能访问。(封装)
- 一个进程通过调用管程的过程进入管程
- 一次只能由一个进程进入管程,其他试图进入将被阻塞。
以下是对管程的互斥的解释:
- wait(c)操作:让进程在条件c的阻塞队列上阻塞,管程现在空闲。所以管程需要主动判断是否阻塞进程,因为wait操作肯定阻塞。
- signal(c)操作:释放一个在条件c上阻塞的进程,如果没有阻塞的,啥也不干。
- 一个在管程中的进程可以调用wait将自己阻塞到条件上,直到有进程signal将它释放。
消息传递
两个以通信进行合作的进程可以通过消息传递进行进程同步,消息的send和receive都是一个原语,那通过消息传递如何同步?
考虑发送方和接收方,发送方可以发完消息继续执行,也可以发完再阻塞直到确认消息发送成功再执行。接收方再receive后也有两种可能,检查到正常收到的消息,那么它取出消息,继续执行,如果没有消息呢,它有两种选择:等待 / 放弃等待继续运行。对此,有几种消息传递机制的组合:
- 阻塞send,阻塞receive:这样进程可以紧密同步,这种情况也成为会和(rendezvous)
- 无阻塞send,阻塞receive:似乎是最好的方案。但是如何确定消息是否收到?则需要程序员实现消息的应答才行。
- 无阻塞send,无阻塞receive:都不等
- 阻塞send,无阻塞receive:不合理,容易导致消息丢失,sender可能完全阻塞。
同时,操作系统提供两种消息寻址方式,
- 一种为直接寻址,发送方直接发到接收方,但是这样必须让双方都知道对方的信息这一方案更加适合两个合作的进程
- 一种为间接寻址,发送方发到共享的信箱中,另外一方再在信箱中寻找自己想要的消息,这在不需要合作的进程间更加有效,比如打印机。
Curry Arthur 2023/04/19 ↩︎