文章目录
1背景
- 并行交互会影响对共享资源的访问
- 独立线程:不和其他线程共享资源这种线程调度顺序不重要,具有确定性和可重复性,即输入决定结果,每次结果保持一致;
- 合作线程:在多个线程中共享状态,具有不确定性和不可重复性。(bug间歇性发生)
- 同步的优点
- 资源共享:多系统控制、交互系统多用户访问等
- 加速:I/O与CPU计算可重叠
- 模块化:模块之间交互
- 同步互斥现象:创建新进程
当并发创建进程时,如果创建进程A时中间发生了上下文切换,进行进程B的创建,此时A和B的进程PID值相同,出现问题。 - 目标:无论多个线程的指令序列怎样交替执行,程序必须正常工作,要求并行程序设计的正确性。操作系统需要利用同步机制,在并发执行的同时保证一些操作是原子操作
2 概念
2.1同步互斥情况
-
竞态条件:多进程/线程系统执行结果依赖于并发执行事件的顺序与时间。会出现不确定性和不可重复性,解决方法是避免同步互斥的部分被打断。
-
Atomic Operation(原子操作):指一次不存在任何中断或者失败的执行。
-
Critical section(临界区):进程中一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会执行的代码区域
-
Mutual exclusion(互斥):当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并访问相同的共享资源
-
Dead lock(死锁):两个或者以上的进程,互相等待完成特定任务,最终没法将自身任务进行下去
-
Starvation(饥饿):一个可执行进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行。
2.2具体现象
-
Lock(锁):锁住临界区,包含“等待”。
-
Unlock(解锁):打开临界区
-
Deadlock(死锁):A有锁1,B有锁2,A向拿锁2后执行,B想拿锁1后执行,此时AB都无法执行 。
-
starvation(饥饿):错误的上下文切换导致两个线程都认为对方会进行操作
-
busy-waiting(忙等待): 忙等待保护了一段临界区 ,只有一段进程访问临界区,有效但是浪费了A资源
-
锁:使用锁,控制加锁与解锁,锁操作是原子操作,如果两个线程都在等同一个锁,锁被释放时只有一个能获得锁。
3临界区实现
3.1属性
- 互斥:同一临界区,最多存在一个进程
- Progress:一个线程想进入临界区,他最后会进入临界区(不会出现死锁)
- 有限等待:线程i在入口处,保证有限时间内能进入临界区,否则会造成饥饿(不会因为抢不到锁一直等待)
- 无忙等待(可选):等待进入临界区,他会在进入之前挂起,不占用CPU资源
3.2实现
3.2.1 基于硬件中断
- 原理:没有中断就没有上下文切换,相当于没有并发,可以原子操作。进入临界区后禁用中断,离开后再启用中断。
- 缺点:中断禁用,硬件IO操作无法及时响应,其他进程处于饥饿。中断时长未知,可能影响硬件。对于并行的两个包含临界区的线程需要全部禁用中断。
3.2.2基于软件方法
- 进入临界区,执行临界区代码,退出临界区,执行剩下代码。
3.2.2.1严格轮换法
- 轮turn进入临界区,存在忙等待现象,等待的进程会一致进行CPU计算,浪费CPU资源(自旋锁)。满足互斥,不满足progress(看给turn为多少)
while(1){
while(turn!=0); //等待到进程0
//lock_enter
//...
turn = 1; //轮转到下一个进程
//lock_exit
}
3.2.2.2Peterson(1981)
std::vector<bool> flag(3,false) ;//是否做好进入的准备
int turn ; //谁进入优先区
do{
//对于进入临界区进程
flag[i] = true;
//进程j进入临界区,等待
turn = j;
while(flag[j]&&turn == j);
//退出临界区
flag[i] = false;
}while(true);
3.2.2.3 dekker(1965)
3.2.2.4N进程解决互斥方法
- Eisenberg and McGuire’si进程前面有进程进入临界区,i等待。i后边的进程等待i进入临界区。
- Bakery算法(1979)
进入临界区之前进程接收一个数字,得到数字最小的进入临界区,当两个进程取到相同的数字,比较进程号来排队。编号方案按枚举的增加顺序生成数字。
软件方法优缺点
缺点:需要忙等待,浪费CPU时间,需要硬件保证
3.2.3更高级的抽象
原理:基于硬件原子抽象的高级实现
锁
一种抽象的数据结构,是一个二进制状态(锁定/解锁)
Lock::Acquire()——锁被释放前一直等待,然后得到锁
Lock::Release()——释放锁,唤醒任何等待的进程
原子操作指令
- Test-and-Set 测试和置位:从内存中读取值,测试该值是否为1,内存值设置为1
- 交换:交换内存中的两个值
- 两个指令被封装成了机器指令,不会发生上下文切换
通过锁编写临界区
Test-and-Set 测试和置位
class Lock{
int value = 0;
//锁被释放,test_and_set读取值0并将值设置为1,锁设置为忙等待完成
//上锁,test_and_set读取1并将值设置为1->不改变锁状态,变成自旋锁(盲等)
Lock::Acquire(){
while(test_and_set(value))
;//spin
}
Lock::Release(){
value = 0;
}
};
- 改进:
可以改为无忙等待,临界区短选忙等,长选挂起
class Lock{
int value = 0;
WaitQueu q;
Lock::Acquire(){
while(test_and_set(value)){
//add this TCB to wait queue q;
schedule();
}
}
Lock::Release(){
value = 0;
//remove one thread t from q
wakeup(t);
}
};
交换:
优点:适用于单处理器或主存共享的多处理器中任意数量的进程,支持多临界区
缺点:多个进程盲等可能导致饥饿(抢交换,有可能一致抢不到)、出现死锁
int lock = 0; //全局共享资源
int key;
do{
key = 1;
while(key==1) exchange(lock,key);
//进入临界区
//...
lock = 0;
//退出临界区
}