一、背景
①多个进程会交互,对共享资源的访问。处理不当就会饥饿,死锁。
②独立的线程:不和其他线程共享资源或状态,不交互,所以具有确定性(输入状态决定结果),可重现(能重现起始条件),I/O,调度顺序不重要
合作的线程:在多个线程中共享状态,不确定性,不可重现
不确定性和不可重现意味着BUG可能是间歇性发生的
③为什么要合作?
共享资源(嵌入式系统);
加速,效率高(I/O操作和计算可以重叠,拆分小任务,流水,并行,多处理器);
模块化:大程序分解成小程序,使系统易于扩展
二、现实中同步问题
1、原子操作 (atomic operation)
指一次不存在任何中断或者失败的执行,要么成功done要么没执行
实际操作往往不是原子的,甚至单个机器指令都不一定是原子的。
2、临界区critical section:
进程中访问共享资源的代码区域,且当另一个进程处于相应代码区域时便不会执行。
3、互斥mutual exclusion:
任一时刻只能有一个进程进入临界区访问。当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区,并访问任何相同的共享资源。
4、死锁Dead lock
多个进程相互等待完成特定任务,而最终没法继续自身任务
5、饥饿starvation:
一个可执行的进程长期等待执行,被调度器持续忽略。
三、临界区和禁用硬件中断同步方法
1、临界区(Critical Section)
①临界区:进程中访问临界资源的一段需要互斥执行的代码
②进入区:检查可否进入临界区的一段代码;如可进入,设置相应“正在访问临界区”标志
③退出区:清除“正在访问临界区”标志
④剩余区:代码中的其余部分
代码:
entry section
critical section
exist section
remainder section
⑤临界区访问规则
Ⅰ、空闲则入:没有进程在临界区时,任何进程可进入
Ⅱ、忙则等待:有有进程在临界区时,其他进程均不能进入临界区
Ⅲ、有限等待:等待进入临界 区的进程不能无限期等待
Ⅳ、让权等待:不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
⑥临界区的实现方法
三种方法:禁用硬件中断,基于软件,原子操作指令
2、禁用硬件中断
①思路:没有中断,就没有上下文切换,没有并发。减少不确定性。进入临界区禁用中断,退出时再开启。
②问题: 一旦禁用中断,线程无法停止 ;整个系统都会停下来,I/O啥的都没用了,其他线程可能会饥饿 影响效率;临界区要是太长咋整?无法限制响应中断所需的时间,可能有硬件影响。一般都用于短的临界区时间。
三、基于软件的同步方法
1、Peterson算法
①共享变量
int turn; // who ‘s turn to enter the critical section bollean flag[]; //whether the process is ready to enter
②进入区代码
flag[i] = TURE; turn=j; while(flag[j]&&turn==j ) ;
③退出区代码
flag[i]=FLASE;
④Peterson算法实现(2个进程)
use two shared data items
int turn; // who ‘s turn to enter the critical section
bollean flag[]; //whether the process is ready to enter
PETERSON算法
do{
flag[i] = TURE;
turn=j;
while(flag[j]&&turn==j ) ;
CRITICAL SECTION
flag[i]=FLASE;
REMAINDER SECTION
} while(TURE);
2、Dekkers算法(与Peretson改入临界区条件)
use two shared data items
int turn=0; // who ‘s turn to enter the critical section
bollean flag[]; //whether the process is ready to enter
flag[0]:=false;flag[1]=false;
Dekkers算法
do{
flag[i] = TURE;
while(flag[j] == turn==j ) {
if(turn!=i){
flag[i] := false
while turn!=i{}
flag[i]:=ture
}
};
CRITICAL SECTION
turn := j;
flag[i]=FLASE;
REMAINDER SECTION
} while(TURE);
3、N线程的软件方法(Eisenberg and Mcguire’s algorirhm)
四、高级抽象的同部方法
硬件提供了一些原语,用原子操作直接实现进退临界区
1、锁是一个抽象的数据结构,获得锁就是进入临界区的实现过程
lock_next_pid->acquire();
new_pid=next_pid++ ;
lock_next_pid->release();
多数现代体系结构都提供特殊的原子操作指令
(1) test-and-set 测试和置位
从内存中读值,判断是否为1并返回,同时设置内存值为1
(2) exchange 交换 输入两个内存单元,交换其值并返回
这两条如果有一条可以是原子指令,就可以完成
class lock{ int value = 0; waitqueue q;}
lock::acquire(){
while (test-and-set(value)){
add this TCB to wait queen q; //加入wait queen list
schedule();}
}
lock::release(){
value=0;
remove one thread t from q;
wakeup(t);
}
2、特征
①、优点:实现简单,易扩展到多临界区,开销小,适用于单处理器或共享主存的多处理器中任意数量的进程 广泛使用
②、缺点: 还是有忙等,浪费时间;
抢LOCK随机可能某个一直抢不到,当进程离开临界区,且多个进程在等待时可能导致饥饿;
也许死锁,一个低优先级的进程拥有临界区,一个高优先级的 进程也需求,那么高优先级进程忙等,占用cpu,低优先级的不能释放Lock,要通过优先级反转来解决。
③总结:
用锁来解决互斥问题,锁是高层编程抽象,需要一定硬件支持
常用三种:禁用中断(仅可单处理器),软件方法(复杂),原子操作指令(单处理器或多处理器都可以)
可选的实现内容:有忙等待,无忙等待(进程睡眠)