背景
- 竞争条件:多个进程并发访问和操作同一个数据并且执行结果与特定访问顺序有关
- 写语句的机器语言表现:
register = para
register = register + value
para = register
临界区问题
临界区条件:
1.互斥:只有一个进程能在临界区内执行
2.进步:如果没有进程在临界区内执行,并且有进程需要进入临界区。则从这些进程中选择一个进程进入临界区,且这种选择不能无限等待。
3.有限等待:一个进程请求进入临界区不会无限等待下去
抢占式内核:允许处于内核模式的进程被抢占
非抢占式内核:不允许处于内核模式的进程被抢占
Peterson解决方案
对于2个进程i,j而言
int turn; 表示哪个进程可以进入临界区
boolean flag[2]; 表示哪个进程准备进入临界区
进程i为了进入临界区,需要将turn设置为j(表示如果进程j希望进入临界区,那么它能够进入),并将flag[i]设置为true
当flag[j] == false 或者 turn == i时,进程i能够进入临界区
否则,turn决定最终哪个进程进入临界区
硬件同步
单处理器环境在修改共享变量时,只要禁止中断出现就能解决临界区问题
- 特殊硬件指令:
test_and_set:返回原始值并设置为true
compare_and_swap:只有当*value == expected时,才设置value为new_value,并返回value原始值
- 满足优先等待要求:
boolean waiting[n];
boolean lock;
线程i为了进入临界区,需要满足waiting[i] == false 或者 key == false
线程退出临界区,循环扫描数组,找到下一个等待的进程作为下次进入临界区的进程,如果没有等待的进程,则将lock设置为false
互斥锁
acquire()获取锁
release()释放锁
自旋锁:当拿不到锁时循环调用acquire
优点:进程等待锁时,没有上下文切换
信号量
wait() / P
signal() / V
- 克服忙等待:
当进程调用wait发现信号量为负时,阻塞自己,将该进程放到与信号量相关的等待队列中
signal从等待进程的链表上取一个进程并唤醒
死锁:多个进程无限等待一个事件,该事件只能由这些等待进程之一来产生
饥饿:进程无限等待信号量
解决方案:优先级继承,即持锁进程的优先级被临时设置为锁等待队列中优先级最高的那个进程的优先级。
经典同步问题
- 缓冲区问题
两个进程共享一个公共的固定大小的缓冲区。其中的一个,生产者,将信息放入缓冲区;另一个,消费者,从缓冲区中取出信息(该问题也可被推广到m个生产者,n个消费者的情况
解决:基于信号量
- 读写锁问题
第一读者-作者问题:读者和作者等待,优先读者
第二读者-作者问题:读者和作者等待,优先作者
数据结构:
semaphore rw_mutex = 1; //读者作者共享锁
semaphore mutex = 1; //确保更新read_count时互斥
int read_count = 0;
- 哲学家问题
五个哲学家,他们终其一生只是在思考和就餐。 他们坐在一张带五把椅子的圆桌旁。 每个位置前面有一大盘米饭。 但是,只有五根筷子可用。 每个哲学家都在思考。 当他饿了,他拿起最接近他的两根筷子,一个哲学家一次只能拿起一根筷子。 如果一个哲学家可以拿起两根筷子,他可以吃一段时间。 在一位哲学家吃完之后,他放下筷子再次开始思考。
同步例子
Linux中,如果内核态的一个任务占有锁,那么内核不能被抢占。
- Pthreads互斥锁
Pthreads分为命名信号量(能被多个进程共享),无名信号量(只能被同一进程的线程使用)
- 哲学家就餐问题
- 读写锁
- 有界缓冲问题(生产者-消费者问题)
- 优先级反转: 如果高优先级的进程需要获取的资源被较低优先级进程占有,此时较低优先级进程被较高优先级进程抢占