1. 互斥与同步的概念
互斥和同步是两个紧密相关而又容易混淆的概念。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
同步:任务间的直接制约关系,A要继续执行需要B完成某一个操作操作才能继续进行。
互斥:任务间的间接制约关系,A访问了资源B就不能去访问,必须等A访问完了才行。
2. 临界区
在并发进程环境中,因为引入了中断,可能导致某些本该原子执行的指令序列被中断,而使得程序运行结果不确定,解决的方法就是保证一次只有一个进程执行这些指令(一次只允许一个进程进入临界区)。
临界区:进程中访问临界资源的一段需要互斥执行的代码。对临界区的访问要确保如下规则:
- 空闲则入:没有进程在临界区时,任何进程可进入
- 忙则等待:有进程在临界区时,其他进程均不能进入临界区
- 有限等待:等待进入临界区的进程不能无限期等待
- 让权等待(可选):不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
临界区的实现方法
(1)方法1:禁用硬件中断(仅限于单处理器)
(2)方法2:基于软件的解决方法(实现复杂)
- 比如:Peterson算法、Dekkers算法、Eisenberg和McGuire算法
(3)方法3:更高级的抽象方法(单处理器或多处理器均可)
硬件提供了一些原子操作指令,比如测试和置位(Test-and-Set )指令、交换指令(exchange),硬件的支持使得我们可以提供更高级的抽象解决方法。比如锁机制。
3. 锁机制
3.1 基本概念
锁是一个更高等级的编程抽象。
包含一个二进制变量(锁定/解锁),两个操作:
Lock::Release()释放锁,唤醒任何等待的进程
Lock::Acquire()锁被释放前一直等待,然后得到锁
3.2 锁的实现
具体实现锁的时候可以实现为两种方式:
(1)有忙等待:在获取锁的时候,如果锁已经被获取了,则进程一直进行空循环,适合临界区执行时间短的情况。
(2)无忙等待。获取锁的时候,如果锁已经被获取了,则进程放到等待队列,CPU进行上下文切换,执行其他进程。适合临界区执行费时的情况。
使用Test-and-Set指令实现Acquire和Release操作:
使用Exchange指令实现进入临界区和退出临界区操作:
3.3 锁的应用
锁机制可以用于互斥问题。
4. 信号量
4.1 信号量的概念
信号量最早提出的时候就是为了解决操作系统中的同步互斥问题。
4.2 信号量的实现
实现类似于锁机制,但有点不同于锁机制,信号量的实现一般是用等待队列。
4.3 信号量的双用途
- 互斥问题
- 同步问题
5. 条件变量
5.1 条件变量的概念
条件变量是管程内的等待机制
进入管程的线程因资源被占用而进入等待状态
每个条件变量表示一种等待原因,对应一个等待队列
Wait()操作
释放锁,让其他线程有机会执行,自己睡眠。
Signal()操作
唤醒等待者。
5.2 条件变量的实现
6.管程
6.1 管程的概念
管程最早提出的时候是针对编程语言层面,不是操作系统层面。
管程的组成:
一个锁:控制管程代码的互斥访问。
0或者多个条件变量:等待/通知信号量用于管理共享数据的并发访问。
6.2 管程条件变量的释放处理方式
Hansen管程当前的优先级更高T2,主要用于真实OS,Java;
Horare管程等待条件变量的优先级更高T1,用于教科书中。
6.3 Java对管程的支持
Object类的如下方法:
void notify() :唤醒在此对象监视器上等待的单个线程。
void notifyAll() :唤醒在此对象监视器上等待的所有线程。
void wait(): 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
7. 生产者消费者问题
(1)用信号量解决
先找出有哪些约束:
互斥约束:一次只能有一个进程操作缓冲区
同步约束:1)缓冲区为空时,消费者必须等待;2)缓冲区满时,生产者必须等待。
然后每个约束用一个单独的信号量。
互斥约束用一个二进制信号量mutex。
两个同步约束分别用两个一般信号量fullBuffers和emptyBuffers
注意:mutex的位置一定要在获取emptyBuffer和fullBuffer之后,否则会死锁。
(2)用管程(锁+条件变量)解决
8. 总结