【操作系统/OS笔记12】同步互斥的三种实现方法:禁用硬件中断、基于软件的解决方案、更高级的抽象

本次笔记内容:
9.5 临界区
9.6 方法1:禁用硬件中断
9.7 方法2:基于软件的解决方案
9.8 方法3:更高级的抽象

文章目录

      • 临界区
      • 管理临界区的方法
        • 方法1:禁用硬件中断
        • 方法2:基于软件的解决方法
        • 算法前置知识与考虑
        • 正确的解决方法(Peterson算法)
        • 更为复杂的dekker算法
        • 针对多进程的Eisenberg and McGuire's Algorithm
        • 针对多进程的Bakery算法
        • 总结
      • 方法3:更高级的抽象
        • 锁是一个抽象的数据结构
        • 硬件的特殊原子操作指令
        • 锁的具体实现
        • 使用Exchange()完成进退临界区
        • 总结
      • 三种方法总结

临界区

临界区中存在的属性:

  • 互斥:同一时间临界区中最多存在一个线程;
  • Progress:如果一个线程想要进入临界区,那么它最终会成功(如果无限等待,处于饥饿状态,不妥);
  • 有限等待:如果一个线程i处于入口区,那么在i的请求被接受之前,其他线程进入临界区的时间是有限制的;
  • 无忙等待(可选):如果一个进程在等待进入临界区,那么它可以进入之前会被挂起。

管理临界区的方法

方法1:禁用硬件中断

采用硬件中断需要考虑时钟中断:时钟中断是控制进程调度的手段之一。

  • 没有中断,没有上下文切换,因此没有并发;
    • 硬件将中断处理延迟到中断被启用之后;
    • 大多数现代计算机体系结构都提供指令来完成。
  • 进入临界区,则禁用中断;
  • 离开临界区,则开启中断。

但是,存在如下问题:

  • 一旦中断被禁用,线程就无法被停止;
    • 整个系统都会为你停下;
    • 可能导致其他线程处于饥饿状态;
  • 要是临界区可以任意长怎么办?
    • 无法限制响应中断所需的时间(可能存在硬件影响);
  • 要小心使用。

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

例子:假设有两个线程,T0和T1。Ti的通常结构为:

do {
	enter section 进入区域
	critical section 临界区
	exit section 离开区域
	reminder section 提醒区域
} while(1);

线程可能共享一些共有的变量来同步他们的行为。下面想设计一种方法,能在有限时间内实现退出/进入页区。

算法前置知识与考虑

  • 共享变量,先初始化
  • int turn = 0;
  • turn == i // 表示谁该进入临界区
  • 对于Thread Ti,代码表示如下:
do {
	while (turn != i) ;  // 如果不是i,死循环,直到turn是i,才可以跳出循环区
	critical section  // 执行临界区代码
	turn = j;  // turn赋为j,退出循环
	reminder section
} while(1);

上述方法满足互斥,但有时不满足progress。

  • Ti做其他事情,Tj想要继续运行,但是必须等待Ti处理临界区。

因此,使用数组flag指示进程是否准备好进入临界区。

对于有线程0、线程1的情况:

  • int flag[2]; flag[0] = flag[1] = 0;
  • flag[i] == 1 // 如果等于1,则线程i进入临界区
  • 对于Thread Ti,代码如下:
do {
	while (flag[j] == 1);  // 如果另一个进程想进来,此进程先谦让一下,自己先循环着
	flag[1] = 1;  // 如果别的进程未准备,则自己赋成1,表示自己要进入临界区
	critical section
	flag[i] = 0;
	reminder section
} while(1);

但是上述方法没有互斥。因此考虑将flag[i] = 1前置,代码如下:

do {
	flag[1] = 1;
	while (flag[j] == 1);
	critical section
	flag[i] = 0;
	reminder section
} while(1);

上述代码满足互斥,但是存在死锁,进程0、1都可能flag为0,谁都跳不出循环。

正确的解决方法(Peterson算法)

满足进程Pi和Pj之间互斥的经典的基于软件的解决方法(1981年),Use two shared data items(用上了turn和flag)。

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

Code for ENTER_CRITICAL_SECTION

	flag[i] = TRUE;
	turn = j;
	while(flag[j] && turn == j);

Code for EXIT_CRITICAL_SECTION

	flag[i] = FALSE;

对于进程Pi的算法:

do {
	flag[i] = TRUE;
	turn = j;
	while (flag[j] && turn == j);
	CRITICAL SECTION
	flag[i] = FALSE;
	REMAINDER SECTION
} while (TRUE);

上述算法能够满足互斥、前进、有限等待三种特性。可以用反证法来证明。

更为复杂的dekker算法

dekker算法的实现如下。

flag[0] := false flag[1] := false := 0 // or 1
do {
	flag[i] = TRUE;
	while flag[j] == true {
		if turn != i {
			flag[i] := false
			while turn != i {}
			flag[i] := TRUE
		}
	}
	CRITICAL SECTION
	turn := j
	flag[i] = FALSE;
	REMAINDER SECTION
} while (TRUE);

此算法特征在课内不展开讨论。

针对多进程的Eisenberg and McGuire’s Algorithm

【操作系统/OS笔记12】同步互斥的三种实现方法:禁用硬件中断、基于软件的解决方案、更高级的抽象_第1张图片

基本思路:对于i进程,如果前面有进程,那么i进程就等待;对于i后面的进程,则等待i。这整体是一种循环。

针对多进程的Bakery算法

N个进程的临界区:

  • 进入临界区之前,进程接受一个数字;
  • 得到的数字最小的进入临界区;
  • 如果进程Pi和Pj收到相同的数字,那么如果i小于j,Pi先进入临界区,否则Pj先进入临界区;
  • 编号方案总是按照枚举的增加顺序生成数字。

总结

  • Dekker算法(1965):第一个针对双线程例子的正确解决方案;
  • Bakery算法(Lamport 1979):针对n线程的临界区问题解决方案。
  • 算法是复杂的:需要两个进程间的共享数据项;
  • 需要忙等待:浪费CPU时间;
  • 没有硬件保证的情况下无真正的软件解决方案:Peterson算法需要原子的LOAD和STORE指令。

方法3:更高级的抽象

  • 硬件提供了一些原语:
    • 像中断禁用,原子操作指令等;
    • 大多数现代体系结构都这样。
  • 操作系统提供更高级的编程抽象来简化并行编程
    • 例如:锁,信号量;
    • 从硬件原语中构建。

锁是一个抽象的数据结构

  • 一个二进制状态(锁定/解锁),两种方法:

    • Lock::Acquire() 锁被释放前一直等待,然后得到锁;
    • Lock::Release() 释放锁,唤醒任何等待的进程
  • 使用锁来编写临界区

  • 前面的例子变得简单起来:

lock_next_pid->Acquire();
new_pid = next_pid++;
lock_next_pid->Release();

硬件的特殊原子操作指令

  • 大多数现代体系结构都提供特殊的原子操作指令
    • 通过特殊的内存访问电路
    • 针对单处理器和多处理器

特殊的原子操作如:

  • Test-and-Set 测试和置位(这是一条机器指令,包含读写两条操作),完成以下三项操作:
    • 从内存中读取值
    • 测试该值是否为1(然后返回真或假)
    • 内存值设置为1
  • 交换Exchange
    • 交换内存中两个值

上述两个指令代码如下:

boolean TestAndSet(boolean *target) {
	boolean rv = *target;
	*target = TRUE;
	return rv;
}
void Exchange(boolean *a, boolean *b) {
	boolean temp = *a;
	*a = *b;
	*b = temp;
}

上述两个指令已经被分别封装为一条机器指令,执行过程中不允许被中断。

锁的具体实现

【操作系统/OS笔记12】同步互斥的三种实现方法:禁用硬件中断、基于软件的解决方案、更高级的抽象_第2张图片

如上图,在锁的方法中,应用原子操作,对描述状态的value进行操作。

【操作系统/OS笔记12】同步互斥的三种实现方法:禁用硬件中断、基于软件的解决方案、更高级的抽象_第3张图片

如上图,该方法存在进程的忙等情况,该如何改进呢?

可以应用睡眠的方法。

无忙等待的锁实现如下:

class Lock {
	int value = 0;
	WaitQueue 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);
}

如果临界区执行时间短,选择忙等方式,因为上下文切换也较大;如果临界区执行时间长,选择无忙等方式。

使用Exchange()完成进退临界区

  • 共享数据(初始化为0)
  • int lock = 0;

对于线程Ti的代码:

int key;
do {
	key = 1;
	while (key == 1) exchange(lock, key);
	critical section
	lock = 0;
	remainder section
} while(1);

总结

优点:

  • 适用于单处理器或者共享主存的多处理器中任意数量的进程;
  • 简单并且容易证明;
  • 可以用于支持多临界区。

缺点:

  • 忙等待消耗处理器时间;
  • 当进程离开临界区并且多个进程在等待的时候可能导致饥饿;
  • 死锁,如一个低优先级的进程拥有临界区,并且一个高优先级进程也需求,那么高优先级进程会获得处理器并且等待临界区。

三种方法总结

【操作系统/OS笔记12】同步互斥的三种实现方法:禁用硬件中断、基于软件的解决方案、更高级的抽象_第4张图片

三种方法要点总结如上图。

你可能感兴趣的:(OS)