同步和互斥
同步和互斥基本概念
临界资源
一次仅允许一个进程使用的资源称为临界资源。
将临界资源的访问过程分成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条件而阻塞的进程。