同步和互斥

同步和互斥

同步和互斥基本概念

临界资源

一次仅允许一个进程使用的资源称为临界资源。

将临界资源的访问过程分成4个部分:

while(true){
	entry section; // 进入区
	critical section; //临界区
	exit section; // 退出区
	remainder section; // 剩余区
}

同步

即进程运行次序的制约关系。直接制约关系。

互斥

一次只能有一个进程访问临界区。间接制约关系。

临界区互斥准则
  • 空闲让进。临界区空闲时,允许请求进入临界区的进程立即进入临界区
  • 忙则等待。已有进程进入临界区,其他试图进入的进程必须等待
  • 有限等待。对请求访问的进程,保证在有限时间内进入临界区,防止进程无限等待
  • 让权等待。当进程不能进入临界区时,立即释放处理器,防止忙等待。(原则上遵循,非必须)

实现临界区互斥的基本方法

软件实现方法

单标志法
  • 设置一个公用整型变量turn,turn=0时,允许P0进入,turn=1时,允许P1进入。

  • 进程退出临界区时将使用权赋给另一个进程。

  • 就有个问题:两个进程必须交替进入临界区,若某个进程不再进入临界区,另一个进程也无法进入。(违背空闲让进原则)

双标志先检查法
  • 设置一个布尔型数组flag[2],标记各个进程想进入临界区的意愿。flag[i]=true表示Pi想进入临界区。
  • Pi进入临界区前先检查对方是否想进入临界区,若想就等待,否则将flag[i]置为true后进入临界区。
  • Pi退出时,flag[i]置为false。
P0:
while(flag[1]);   1
flag[0]=true;     3
critical section;
flag[0]=false;
remainder section;

P1:
while(flag[0]);  2
flag[1]=true;    4
critical section;
flag[1]=false;
remainder section;

当按照1234的顺序执行时,可能出现P0和P1同时进入临界区,原因在于检查和设置不是原子的。

双标志后检查法

先设置后检查。

P0:
flag[0]=true;     1
while(flag[1]);   3
critical section;
flag[0]=false;
remainder section;

P1:
flag[1]=true;    2
while(flag[0]);  4
critical section;
flag[1]=false;
remainder section;

按照1234,死锁。

Peterson算法

若双方争着进入临界区,则将进入临界区的机会谦让给对方。

  • 在每个进程进入临界区前,先设置自己的flag标志,再设置允许进入turn标志
  • 再同时检测对方的flag和turn标志,保证双方同时要求进入临界区时,只允许一个进程进入。
P0:
flag[0]=true;
turn=1;
while(flag[1] && (turn==1));
critical section;
flag[0]=false;
remainder section;

P1:
flag[1]=true;
turn=0;
while(flag[0] && (turn==0));
critical section;
flag[1]=false;
remainder section;

硬件实现方法

中断屏蔽方法
  • 防止其他进程进入临界区的最简单方法就是关中断。
  • cpu只在发生中断时引起进程切换,屏蔽中断能保证当前运行的进程让临界区代码顺利执行完,进而保证互斥正确实现,然后执行开中断。
缺点
  • 限制cpu交替执行程序能力
  • 将关中断权限交给用户很不明智
  • 不适用多处理器系统,在一个cpu上关中断不能防止在其他cpu上执行相同临界区代码。
硬件指令方法TestAndSet指令
  • 借助硬件指令TestAndSet指令实现互斥,该指令是原子的。
  • 功能是读出指定标志将该标志设置为真。
while TestAndSet(&lock);
进程的临界区代码段;
lock=false;
进程的其他代码;

缺点:暂时无法进入临界区的进程会占用cpu循环执行ts指令,因此不能让权等待。

硬件指令方法Swap指令
  • 为每个临界资源设置一个共享布尔变量lock,初值为false;
  • 每个进程设置一个局部布尔变量key,初值为true,用于和lock交换信息。
boolean key = true;
while(key != false) Swap(&lock, &key);
临界区代码
lock=false;
其他代码
硬件指令实现互斥
优点
  • 简单、容易验证正确性
  • 适用于任意数目进程,支持多处理系统
  • 支持系统中有多个临界区,只需为每个临界区设立一个布尔变量
缺点
  • 不能实现让权等待
  • 从等待进程随机选择一个进程进入临界区,可能导致饥饿

互斥锁

  • 解决临界区最简单工具是互斥锁(mutex lock)
  • 进程进入临界区时调用acquire(),退出时调用release()函数
  • acquire和release必须是原子操作,互斥锁通常采用硬件机制来实现
  • 互斥锁也称自旋锁,会忙等待。
  • 通常用于多处理器系统,一个线程可以在一个处理器上选择,不影响其他线程执行
  • 进程等待锁期间没有上下文切换,若上锁时间较短,则等待代价不高

信号量

  • 只能被两个标准原语wait()和signal()访问
  • 原语是被某种功能不被分割、不被中断执行的操作序列,通常由硬件实现。原语之所以不能被中断,因为原语对变量的操作过程若被打断,可能会去运行另一个对同一变量的操作过程,从而出现临界段问题
整型信号量
wait(S){
	while(S<=0);
	S=S-1;
}
signal(S){
 	S=S+1;
}
记录型信号量

一种不存在忙等现象的进程同步机制

  • 需要一个代表资源数目的整型变量value,再增加一个进程链表L,用于链接所有等待该资源的进程
typedef struct{
	int value;
	struct process *L;
} semaphore;

void wait(semaphore S){
	S.value--;
	if(S.value<0){
		add this process to S.L;
		block(S.L);
	}
}

void signal(semaphore S){
	S.value++;
	if(S.value<=0){
		remove a process P from S.L;
		wakeup(P);
	}
}
  • 当S.value<0时,表示该类资源已分配完毕,因此调用block原语进行自我阻塞,主动放弃cpu,插入该类资源的等待队列S.L,遵循让权等待。
  • 若加1后仍然S.value<=0,表示仍有进程在等待该类资源,因此调用wakeup原语将S.L中第一个进程唤醒
用信号量实现进程互斥
semaphore S = 1;
P1(){
	P(S);
	进程P1临界区
	V(S);
}
P2(){
	P(S);
	进程P2临界区
	V(S);
}
信号量实现同步
semaphore S=0;
P1(){
	x; // 执行语句x
	V(S); //告诉进程P2,语句x完成
	...
}
P2(){
	...
	P(S); //检查语句x是否运行完成
	y;
}

经典同步问题

生产者-消费者问题

  • 一组生产者和消费者进程共享一个初始为空、大小为n的缓冲区
  • 只有缓冲区不满生产者才能将消息放入缓冲区
  • 缓冲区不空,消费者才能从缓冲区取出消息
关系分析
  • 生产者和消费者对缓存区互斥访问时互斥关系
  • 生产者生产后消费者才能消费,满了之后要消费才能生产,是同步关系
semaphore mutex = 1
semaphore empty=n //空闲缓冲区
semaphore full = 0
producer(){
	while(1){
		produce an item in nextp;
		P(empty);
		P(mutex);
		add nextp to buffer;
		V(mutex);
		V(full);
	}
}

consumer(){
	while(1){
		P(full);
		P(mutex);
		remove an item from buffer;
		V(mutex);
		V(empty);
		consume the item;
	}
}
  • 如果先P(mutex)再P(emtpy),当缓存区满的时候,P(empty)阻塞,但是由于生产者已经封锁mutex,消费者也被阻塞,死锁了。

读者-写者问题

  • 读不互斥,只允许一个写者写
  • 在写者完成写操作之前不允许其他读者或写者工作
  • 写者执行写操作前,让已有的读者和写者全部退出
关系

读写、写写互斥,读读不互斥

int count = 0; // 读者数量
semaphore mutex = 1;  // 保护更新count变量时的互斥
semaphore rw = 1; //保证读者写者互斥访问文件

writer(){
	P(rw); //互斥访问共享文件
	writing;
	V(rw);
}

reader(){
	while(1){
		P(mutex);
		if(count == 0)P(rw); // 第一个读进程读时,阻止写进程写
		count++;
		V(mutex);
		reading;
		P(mutex);
		count--;
		if(count == 0)V(rw);
		V(mutex);
	}
}

保证在有写访问时,禁止后面的读请求

int count = 0; // 读者数量
semaphore mutex = 1;  // 保护更新count变量时的互斥
semaphore rw = 1; //保证读者写者互斥访问文件
semaphore w = 1; //实现写优先

writer(){
	P(w);
	P(rw); //互斥访问共享文件
	writing;
	V(rw);
	V(w);
}

reader(){
	while(1){
		P(w);
		P(mutex);
		if(count == 0)P(rw); // 第一个读进程读时,阻止写进程写
		count++;
		V(mutex);
		V(w);
		reading;
		P(mutex);
		count--;
		if(count == 0)V(rw);
		V(mutex);
	}
}

哲学家进餐问题

有五个哲学家围在一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和物质筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两支筷子时才能进餐。进餐完毕后,放下筷子继续思考。

在这里插入图片描述

semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1;
Pi(){
	do{
		P(mutex);
		P(chopstick[i]);
		P(chopstick[i+1%5]);
		V(mutex);
		eat;
		V(chopstick[i]);
		V(chopstick[i+1%5]);
		think;
	}while(1);
}

管程

管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生可能性,同时管程提供条件变量,可以让程序员灵活实现进程同步。

定义描述举例如下:

monitor Demo{
	共享数据结构S;
	
	init_code(){
		S=5;
	}
	
	take_away(){
		一系列处理
		S--;
		...
	}
	
	give_back(){
		一系列处理
		S++;
		...
	}
}
  • 管程将对共享资源的操作封装起来,管程内共享数据结构只能被管程内过程访问
  • 每次仅允许一个进程进入管程,从而实现进程互斥
条件变量

将阻塞原因定义为条件变量Condition,每个条件变量保存了一个等待队列,记录因该条件变量而阻塞的所有进程,对条件变量只能wait和signal

  • x.wait:当x对应的条件不满足,正在调用管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程。其他进程可以使用该管程
  • x.signal:x对应的条件发生变化,则调用x.signal,唤醒一个因x条件而阻塞的进程。
  • 12
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值