同步互斥的实现

一、临界区

1.定义:临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用。

2.临界区中存在的属性:

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

 

二、管理临界区的方法

1. 禁用硬件中断

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

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

但是,存在如下问题:

  • 一旦中断被禁用,线程就无法被停止;整个系统都会为你停下;可能导致其他线程处于饥饿状态。
  • 如临界区可以任意长,则无法限制响应中断所需的时间

 

2. 基于软件的解决方法

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

1 do{
2      enter section            //进入区域
3      critical section          //临界区
4      exit section              //离开区域
5      reminder section      //提醒区域
6 }while(1);

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

算法前置知识与考虑

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

上述代码满足互斥,即不可能两个线程同时进入临界区。但不满足process,比如T1执行完进入临界区代码后,不再进入临界区程序,转去执行其他任务。而T2执行完临界区代码后,想再次进入临界区,而发现自己在退出临界区时,
把turn赋值为了1,因此再执行进入临界区代码时,由于turn=1而不是2,会执行死循环而不能进入临界区。此方法的特点就是必须T1和T2交替执行来改变turn值,才能满足process。

因此再考虑其他方法:

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

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

该方法没有实现互斥,如T1执行完前两条代码后,上下文切换到T2,T2执行完前两条代码后 flag[1] == flag[2] ==0 ,所以两个线程都能进入临界区,不满足互斥。

考虑将flag[i] = 1前置,代码如下

1 do{
2        flag[i] = 1; 
3        while (flag[j] == 1);  
4        critical section
5        flag[i] = 0;
6        reminder section
7 }while(1);

此方法满足互斥,但可能出现死锁,
如T1执行完前两条代码后,上下文切换到T2,T2执行完前两条代码后 flag[1] == flag[2] ==1,两个进程都会进入死循环,所以两个线程都不能进入临界区。

 

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

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

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

Code for ENTER_CRITICAL_SECTION

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

Code for EXIT_CRITICAL_SECTION

flag[i] = FALSE;

对于进程Pi的算法:

1 do {
2     flag[i] = TRUE;
3     turn = j;
4     while (flag[j] && turn == j);
5     CRITICAL SECTION
6     flag[i] = FALSE;
7     REMAINDER SECTION
8 } 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

 

 

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

 

针对多进程的Bakery算法
N个进程的临界区:

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

总结

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

方法3:更高级的抽象



  

猜你喜欢

转载自www.cnblogs.com/cjsword/p/12194448.html