带*是重点
临界区: 一个时间段内只允许一个进程使用的资源被称为临界资源,而对临界资源进行访问的那段代码被称为临界区。
同步: 直接制约关系,协调进程的工作次序.
互斥: 间接制约关系,多个进程在同一时刻只有一个进程能进入临界区。
进程同步是为了解决进程特征中的异步性。
需要遵循的规则
1. 软件实现
2. 硬件实现
前面互斥实现的方式都无法实现让权等待原则,而信号量机制则符合所有原则。
概念
信号量是一个变量(整数/记录型),和单标志法一样用于表示系统中某种资源的数量,可以用操作系统提供的一对原语来对信号量进行操作,从而实现进程的互斥同步。
原语
互斥实现
1.整形
int S = 1;//一台打印机
void wait(int S){
while(S<=0);
S=S-1;
}
void signal(int S){
S=S+1;
}
//进程
...
wait(S);//P操作
//使用打印机
signal(S);//V操作
2.记录型
typedef struct {
int value;//剩余资源数
Struct process *L;//等待队列
} semaphore;
void wait(semaphore S){
S.value--;
if(S.value<0)
block(S.L);//block原语阻塞队列,由于进入阻塞态,所以遵循了 让权等待
}
void signal(semaphore S){
S.value++;
if(S.value<=0){
wakup(S,L);//唤醒等待队列中的第一个进程
}
}
3.总结
semaphore mutex = 1;//初始化信号量(简写形式)
semaphore mutex2 = 2;//不同的临界资源需要设置不同的互斥信号量
void P1(){
...
P(mutex);
//临界区...
V(mutex);
...
}
void P2(){
...
P(mutex);
//临界区...
V(mutex);
...
}
同步实现
同步即要让各并发进程按要求有序地进行。
semaphore S = 0;
void P1(){
...
...
V(mutex);//S++;
...
}
void P2(){
P(mutex);//S--,由于初始 S=0,所以必须 P1 先执行完,调度到P2时才会执行
...
...
}
生产者-消费者模型
问题:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
semaphore mutex = 1;//互斥量
semaphore empty = n;//缓冲区
semaphore full = 0;//产品
void producer(){
while(true){
//生产一个产品
P(empty);//消耗一个缓冲区空间
P(mutex);//互斥锁
//放入产品
V(mutex);
V(full);//增加一个产品
}
}
void consumer(){
while(true){
P(full);
P(mutex);
//拿走产品
V(mutex);
V(empty);
}
}
注:互斥的P操作一定要在同步P操作之后,不然会死锁;V操作的顺序则可以任意调换。
多生产者-多消费者
问题:使用一个缓冲区来保存物品,多个生产者生产不同的产品,多个消费者使用不同的产品。
互斥:
生产者和消费者之间都是互斥的
同步:
消费者释放一个缓冲区->生产者消耗一个缓冲区
生产者生产一个苹果->对应生产者拿走一个苹果
生产者生产一个橘子->对应生产者拿走一个橘子
semaphore mutex = 1;
semaphore apple = 0;
semaphore orrange = 0;
semaphore plate = 1;
void father(){
//生产一个苹果
P(plate);
P(mutex);
//将苹果放入盘子
v(mutex);
V(apple);
}
void mother(){
//生产一个橘子
P(plate);
P(mutex);
//将橘子放入盘子
v(mutex);
V(orrange);
}
void son(){
P(apple);
P(mutex);
//取走苹果
V(mutex);
V(plate);
//吃掉苹果
}
void dau(){
P(orrange);
P(mutex);
//取走橘子
V(mutex);
V(plate);
//吃掉橘子
}
注:如果缓冲区大小为1,那么可能不需要设置互斥量就可以实现互斥访问缓冲区的功能。
读者-写者(不是所有互斥)
问题:允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
互斥:
写进程-写进程、写进程-读进程
读进程之间是不互斥的,用一个计数器来实现。
int count = 0;
semaphore countMutex = 1;
semaphore rdMutex = 1;
void writer(){
P(rdMutex);
//写入
V(rdMutex);
}
void reader(){
P(countMutex);
if(count==0)
P(rdMutex);
count++;
V(countMutex);
//读取
P(countMutex);
count--;
if(count==0)
P(rdMutex);
V(countMutex);
}
注:这是一种读优先策略,如果一直有读者加入,则最终会造成写饥饿。
semaphore chopstick[5]={1,1,1,1,1};
void P(int i){//第i个哲学家
while(true){
think();
P(chopstick[i]);//拿起左筷子
P(chopstick[(i+1)%5]);//拿起右筷
eat();
V(chopstick[i]);
V(chopstick[(i+1)%5]);
}
}
解决方案:
1.最多允许四个哲学家同时进餐,这样可以至少保证有一个哲学家拿到两只筷子。
实现:
semaphore nums = 4;
2.奇数号哲学家先拿左边筷子,然后再拿右边筷子,而偶数号哲学家相反。
实现:增加奇偶判断,调整筷子顺序
3.仅当一个哲学家左右两只筷子都可用时,才允许拿起筷子。(较好解法)
实现:
semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1;
void P(int i){//第i个哲学家
while(true){
think();
P(mutex);
P(chopstick[i]);
P(chopstick[(i+1)%5]);
V(mutex);
eat();
V(chopstick[i]);
V(chopstick[(i+1)%5]);
}
}
管程是为了解决信号量在临界区的 PV 操作上的配对的麻烦,把配对的 PV 操作集中在一起,生成的一种并发编程方法。其中使用了条件变量这种同步机制。
特征
作用
解决信号量机制编程麻烦、易出错的问题。
互斥: 由编译器实现。
同步: 管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
用管程解决生产者消费者问题
1.定义管程
monitor PridycerCOnsumer
condition full,empty;
int count = 0;
void insert(Item item){
if(count == N)
wait(full);
count++;
insert_item(item);
if(count == 1)
signal(empty);
}
Item remove(){
if(count==0)
wait(empty);
count--;
if(count == N-1)
signal(full);
}
2.生产者和消费者
producer(){
while(true){
item = 生产一个产品;
ProducerConsumer.insert(item);
}
}
consumer(){
while(true){
item = ProducerConsumer.remove();
消费一个产品
}
}
Java 中的管程
在Java虚拟机中,每个对象和类在逻辑上与管程(对象头在有锁时关联monitor)相关联。而为了实现管程的互斥能力,一个锁关联每个对象和类,Java 中的 Lock 互斥锁是一个二进制信号量,相当于操作系统中的信号量。同样在编码时会不方便
于是 JVM 自动为我们实现了 synchronized ,一旦代码被嵌入synchronized关键字,它就是一个管程区域。该锁在后台通过JVM自动实现。
我们知道每个对象/类都关联一个管程。我认为更好的说法应该是每个对象都有一个管程,因为每个对象可以有它自己的临界区,并能够监控线程顺序。
为了使不同的线程协作,JAVA为提供了wait()和notify()来挂起线程和唤醒另外一个等待的线程
java使用管程解决生产者消费者问题:
static class MyMonitor {
private Item buffer[] = new Item[N];
private int count = 0, lo = 0, hi = 0;// 计数器和索引
public synchronized void insert(Item item) {
if (count == N)
go_to_sleep();
buffer[hi] = itme;
hi = (hi + 1) % N;
count++;
if (count == 1)
notify();
}
public synchronized Item remove() {
Itme val;
if (count == 0)
go_to_sleep();
val = buffer[lo];// 从缓冲区中取出一个数据项
lo = (lo + 1) % N;// 设置待取数据项的槽
count--;
if (count == N - 1)
notify();
return val;
}
private void go_to_sleep() {
try {wait();} catch (InterruptedException exc) {};
}
}
概念
如果一个进程集合里面的每个进程都在等待只能由这个集合中的其他一个进程(包括他自身)才能引发的事件,这种情况就是死锁。
造成死锁的原因就是多个线程或进程对同一个资源的争抢或相互依赖。
资源可分为两种:可剥夺资源和不可剥夺资源。
一般来说对于由可剥夺资源引起的死锁可以由系统的重新分配资源来解决,所以一般说的死锁都是由于不可剥夺资源所引起的。
死锁的必要条件
死锁的处理方法
预防死锁
避免死锁
安全序列和安全状态:
所以我们可以通过一种算法在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,这种算法就是银行家算法:
死锁的检测和解除
检测系统是否已经发生了死锁:
解除死锁的方法: