参考连接
前提知识
临界资源
虽然多个进程可以共享系统中的各种资源,但其中许多 资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源
许多 物理设备 都属于临界资源,如打印机等。此外,还有许多 变量、数据 等都可以被若干进程共享,也属于临界资源
临界区
对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区
互斥
只有一个线程能访问临界区
PV操作
P操作 表示申请一个资源
P操作的定义:S=S-1
- 若S>=0,则执行P操作的线程继续执行
- 若S<0,则置该线程为阻塞状态,并将其插入阻塞队列
V操作 表示释放一个资源
V操作定义:S=S+1
- 若S>0则执行V操作的线程继续执行
- 若S<0,则从阻塞状态唤醒一个线程,并将其插入就绪队列
如果 S初始值为1,那么这个semaphore就是一个mutex semaphore,效果就是临界区的 互斥访问
如果 S初始值为0,那么 做条件同步,效果就是必须等待某些条件发生
如果 S初始值为N(N一般大于1),那么 用来限制并发数目,也被称之为counting semaphone
采用信号量控制线程的 例子:
信号量设置的是2,也就是同时只允许2个线程处理,当第三个线程 T3 来的时候 T1、T2 还没处理完的情况下,T3 会阻塞到 T1 或 T2 执行完成并且通过 V 操作(加1,释放一个位置给别人),这个时候 T3 进行 P操作(减一,把这个位置占用)
锁机制的实现方案有两种:
信号量(Semaphere)
- 操作系统 提供的一种 协调共享资源访问 的方法
- 和用软件实现的同步比较,软件同步是平等线程间的的一种同步协商机制,不能保证原子性
- 信号量则由操作系统进行管理,地位高于进程,操作系统 保证信号量的原子性
管程(Monitor)
- 解决信号量在临界区的 PV 操作上配对的麻烦,把配对的 PV 操作集中在一起,生成的一种并发编程方法,其中使用了 条件变量 这种同步机制
-
信号量将共享变量 S 封装起来,对共享变量 S 的所有操作都只能通过 PV 进行,这和面向对象的思想很像
-
事实上,封装共享变量是并发编程的常用手段
-
在信号量中,当 P 操作无法获取到锁时,将当前线程添加到同步队列(syncQueue)中。当其余线程 V 释放锁时,从同步队列中唤醒等待线程。但当有多个线程通过信号量 PV 配对时会异常复杂,所以管程中引入了等待队列(waitQueue)的概念,进一步封装这些复杂的操作
信号量(Semaphere)
原理
信号中包括一个整形变量,和两个原子操作 P 和 V。其原子性由操作系统保证,这个整形变量只能通过 P 操作和 V 操作改变
共享变量 S 只能由 PV 操作,PV 的原子性由操作系统保证
分类
二进制信号量
:资源数目为 0 或 1
资源信号量
:资源数目为任何非负值
使用场景
互斥访问
实现临界区的互斥访问注意事项:
一是信号量的初始值必须为 1;二是 PV 必须配对使用
Semaphore mutex = new Semaphore(1);
mutex.P();
// do something
mutex.V();
临界值
实现临界区的条件访问注意事项:
初始信号量必须为 0,这样所有的线程调用 P 操作时都无法获取到锁,只能进行等待队列(相当于管程中的等待队列),当其余线程 B 调用 V 操作时会唤醒等待线程
Semaphore condition = new Semaphore(0);
// ThreadA,进行等待队列中
condition.P();
// ThreadB,唤醒等待线程 ThreadA
condition.V();
阻塞队列
阻塞队列是典型的 生产者-消费者模式,任何时刻只能有一个生产者线程或消费都线程访问缓冲区。并且当缓冲区满时,生产者线程必须等待,反之消费者线程必须等待。
任何时刻只能有一个线程操作缓存区:互斥访问,使用二进制信号量 mutex,其信号初始值为 1。
缓存区空时,消费者必须等待生产者:条件同步,使用资源信号量 notEmpty,其信号初始值为 0。
缓存区满时,生产者必须等待消费者:条件同步,使用资源信号量 notFull,其信号初始值为 n
管程(Monitor)
在 并发编程
领域,有两大核心问题:(这两大问题,管程都是能够解决的)
互斥 ,即同一时刻只允许一个线程访问共享资源;
同步 ,即线程之间如何通信、协作
管程指管理共享变量以及对共享变量的操作过程,让他们支持并发
管程和信号量关于互斥的实现完全一样,都是 将共享变量及其操作统一封装起来,任一时刻只有一个线程在执行管程代码
条件变量
条件变量(condition variable)是管程内部的实现机制,每个条件变量都代表一种 等待的原因,也对应一个等待队列
- 条件变量有两个操作:wait和signal,或者在加上一个signal_all操作。条件变量都是配合互斥锁一起使用,互斥锁保证了对临界资源的互斥访问
- 简单来说,管程就是互斥锁(称之为monitor’s lock)与条件变量的配合使用
正在管程内的线程可以放弃 对管程的控制权,等待某些条件发生再继续执行
- 不管互自旋锁还是信号量,进入了临界区域除非代码执行完,否则是不会出现线程切换的
- 管程可以主动放弃执行权,这反映到编码上也会有一些差异
信号量 & 管程
- 为了实现阻塞队列的功能,即等待-通知(wait-notify),除了使用互斥锁 mutex 外,还需要两个判断队满和队空的资源信号量 fullBuffers 和 emptyBuffers,使用起来不仅复杂,还容易出错
- 管程在信号量的基础上,更进一步增加了 条件同步,将上述复杂的操作封起来
- 信号量本质是 可共享的资源的数量
- 管程是一种 抽象数据结构 用来限制同一时刻只有一个线程进入临界区
- 信号量是可以 并发 的,并发量取决于S初始值
- 管程内部同一时刻 最多只能有一个 线程执行
- 信号量的 V操作 如果唤醒了其他线程,当前线程与被唤醒线程 并发执行
- 对于管程的 signal操作,要么当前线程继续执行(Hansen),要么被唤醒线程继续执行(Hoare),二者不能并发
- 信号量 与管理的资源紧耦合(即信号量S的初始值等同于资源的数目,且通过P V操作修改剩余可用的资源数量)
- 在管程中需 自行判断 是否还有可共享的资源
- 信号量的 P操作可能阻塞,也可能不阻塞
- 管程的 wait操作一定会阻塞