【操作系统】第九章——互斥与同步

一、背景

1、什么是独立线程、合作线程

  • 区分他们的主要根据:是否共享资源
  • 详细介绍:

(1)独立线程:不与其它线程共享资源或状态

具有确定性和重现性,也就是输入相同的内容,保证得到预期的结果,整个线程执行过程中,不受外界影响

(2)合作线程:多个线程共享资源

也就会导致出现不确定性、不可重现,还可能出间歇性出现bug。

  • 那么我们为什么还需要合作线程呢?

因为线程一般都是完成单一的任务,有些复杂的功能需要多个线程合作才能完成

2、进程或线程的合作有哪些优点呢?

  • 最基本的出发点是 计算机/设备之间需要合作
  • 第一个优点:可以共享某些资源【例如:一台电脑可能注册多个用户】
  • 第二个优点:加速执行速度【例如:多处理器可以并行执行、I/O操作和计算重叠部分完成一次就可以】
  • 第三个优点:模块化【系统更容易扩展、可以将大程序分为多个小程序方便管理】

3、案例分析:

【操作系统】第九章——互斥与同步_第1张图片

如果是单一进程,这种一个进程分配一个id,id自增的机制没有什么问题

【操作系统】第九章——互斥与同步_第2张图片

但是如果多个进程同时对id进行修改,就可能出现问题:

对于此案例,调度的位置具有随机性,在执行id修改指令时,如果产生了上下文切换,其他进程来进行id修改,但是上一个进程的id没有变,所以两个进程得到的新id是相同的。也就意味着最终的结果具有不确定性 和 不可重复性。

我们是不希望出现这样的结果的,所以我们要满足以下两个条件:

(1)无论多个线程的指令序列怎样交替执行,程序都必须正常工作

(2)不确定性要求并行程序的正确性

4、为什么会出现不确定、不可重现的现象?

因为程序处于竞态条件,进程结果依赖于并发执行或者时间的顺序/时间。

5、那么如何去避免竞态呢?

  • 首先引入原子操作的概念(Atomic Operator):

(1)指不会被线程调度机制打断的操作【整个原子操作不能被打断】

(2)要么完全执行,要么就不执行【操作的最小单位】

  • 但很多逻辑上的操作实际不是原子操作,例如加减操作

【操作系统】第九章——互斥与同步_第3张图片

实际进行一次加法或减法是三条指令构成,所以在没有严格划分临界区时,这个案例的结果是不确定的

6、引入几个其他概念:

  • 临界区:每个进程中访问临界资源的那段程序称为临界区【临界区里只能有一个进程】
  • 互斥:当有进程访问临界区时,其他进程不能访问临界区中的资源
  • 死锁:多个进程处于相互等待的状态,谁也无法继续执行
  • 饥饿:一个进程处于就绪状态,但是一直得不到CPU的处理

三、临界区

1、临界区有哪些属性?

(1)互斥:同一时间临界区中最多存在一个线程

(2)前进:如果一个线程想要进入临界区,那么它最终会成功

(3)有限等待:如果一个线程i处于入口区,那么在i请求被接受之前,其他线程进入临界区的时间是有限制的

(4)无忙等待(可选):如果一个进程在等待进入临界区,那么在它可以进入之前会被挂起

2、是否可以设计一些方法对临界区代码的保护呢?

  • 方法1:禁用 硬件中断

  • 方法2:基于软件的解决方法

  • 方法3:更高级的抽象【硬件原子操作的一种指令

  • 我们将从性能和实现的复杂性来比较这几种方法

四、禁用硬件中断

3、禁用硬件中断时如何实现的呢?

  • 基于:中断被禁用后就不会进行上下文切换,进而就不会有并发的进程

  • 主要就是通过进入临界区禁用中断,离开临界区开启中断

  • 存在的缺点:

(1)中断的作用是用于相应外部设备的时间,如果禁用中断之后,整个系统都会停下来,也就可能导致其他线程处于饥饿状态

(2)如果临界区任意长,那么就会导致无法限制响应中断所需的时间【对系统影响很大】

(3)所以我们应该小心使用,适用于临界区很小的情况,不适用多CPU的情况


五、基于软件的解决方法

1、如何来实现基于软件的解决方案呢?

  • 线程可能共享一些变量来同步他们的行为
    【操作系统】第九章——互斥与同步_第4张图片

  • 方案一:

//设置共享变量,初始化
int turn = 0
//turn 为几就为哪个进程进入临界区
turn == i
//Thread Ti
do{
  //如果没有临到进程i访问临界区就一直等待
  while(turn != i);
  critical section //执行临界区的代码
  turn = j; //轮到j可以访问临界区的资源了
  reminder section //进程i执行剩余的低吗
}while(1);

算法分析:

(1)可以满足进程互斥

(2)但是无法满足前进(谁想访问临界区最后就一定访问的到临界区)

如果进程0在等待临界区访问权限太久,可能就不再想去访问临界区,当进程1访问完临界区,把权限给到进程0,但是进程0不想访问临界区了,但是进程1还想访问临界区,就导致进程1想访问临界区但是却达不到目的

  • 方案二:

【操作系统】第九章——互斥与同步_第5张图片

算法分析:

尽管实现了前进,但是不满足互斥,最开始进程都为0,都可以进入临界区

  • 方案三:
    【操作系统】第九章——互斥与同步_第6张图片

算法分析:

满足了互斥,但是存在死锁,在进程开始时flag标志都会变为1,则会都阻塞在循环这里

2、Perterson算法实现原理:

结合以上三种方案,采用了两个共享数据项

int turn; //指示谁该进入临界区

boolean flag[]; //指示进程是否准备好进入临界区

进入临界区部分代码

flag[i] = TRUE;
turn = j;
//此处说明进程j在访问临界区,所以阻塞进程i[属于通过别人的状态来考虑自己]
while(flag[j] && turn == j);

退出临界区部分代码

//与上面进入临界区的代码是对应的
flag[i] = FALSE;

完整代码

【操作系统】第九章——互斥与同步_第7张图片

3、 Dekker算法(了解):

【操作系统】第九章——互斥与同步_第8张图片

4、 针对N个进程的互斥保护——Eisenberg and McGuire算法

【操作系统】第九章——互斥与同步_第9张图片

5、 针对N个进程的互斥保护——Beakery算法:

(1)在进程进入临界区之前会获得一个编号,谁编号小谁先访问临界区,如果两个进程接收到的数字相同,那么谁id小谁先访问

(2)编号方案是按照枚举增加顺序生成数字

  • 总结:

【操作系统】第九章——互斥与同步_第10张图片


六、更高级的抽象

1、基于硬件原子操作的解决方案:
【操作系统】第九章——互斥与同步_第11张图片

  • 锁是一个抽象的数据结构:

(1)一个二进制状态,有锁定和解锁两种方法

(2)Acquire(): 锁被释放前一直等待,然后得到锁【锁没释放,处于临界区之外的进程无法访问临界区】

(3)Release():释放锁,可以唤醒任何等待的进程

  • 那么如何用锁来编写临界区的代码呢?
lock_next_pid -> Acquire();
new_pid = next_pid++;
lock_next_pid -> Release();
  • 如何来实现这种锁呢?

(1)大多数现代体系结构都提供特殊的原子操作指令

通过特殊的内存访问电路,针对单处理器和多处理器

(2)Test-and-Set 测试和置位指令

从内存中读取值,测试该值是否为1(然后返回真或假)、内存值设置为1

boolean TestandSet(boolean *target){
  boolean rv = *target;
  *target = true;
  return rv;
}

(3)交换指令:交换内存中的两个值

void Exchange(boolean *a, boolean *b)
{
  boolean temp = *a;
  *a = *b;
  *b = temp;
}

2、基于TS指令实现自旋锁(spinlock)

【操作系统】第九章——互斥与同步_第12张图片

  • 如何解决进程忙等的情况呢?

【操作系统】第九章——互斥与同步_第13张图片

(1)使处于忙等的进程睡眠,在临界区执行完的进程将睡眠的进行唤醒

(2)根据临界区的执行时间长短决定采用哪种方式

如果临界区执行时间短 >> 忙等待

如果临界区执行时间长 >> 无忙等待

7、基于Exchange指令实现:

//初始化共享数据
int lock = 0;
//当前线程为 Ti
int key;
do{
  key = 1;
  while(key == 1) exchange(lock, key);
  //临界区
  critical section
  lock = 0;
  //剩余部分
  remainder section
}while(1)

8、原子操作指令锁的特征:

(1)优点

  • 简单,适用于多进程、单处理器、共享主存的多处理器
  • 支持多临界区

(2)缺点

  • 可能出现进程离开临界区时有多个等待进程的情况,也就导致部分进程会出现饥饿的现象
  • 忙等待会消耗处理器时间
    【操作系统】第九章——互斥与同步_第14张图片

9、总结:

(1)基于硬件的原子操作可以利用锁来实现线程/进程之间的互斥

(2)基于禁用中断的锁更适用于单处理器、软件实现的锁比较复杂、更推荐硬件原子操作实现的锁

(3)可采用有忙等待和无忙等待两种方案

你可能感兴趣的:(操作系统,java,算法,前端)