操作系统原理:临界区、信号量、管程的同步和互斥

    临界区:要访问(读写)的共享资源那段代码称之为临界区。这里的代码并不是指C语言等高级语言的代码,而是指机器语言的代码。

    互斥:当线程处于临界区并访问共享资源时,其他线程将不会访问相同的共享资源。

    锁 :对资源加上一种保护机制,使得外部程序无法进行访问。对应的反向操作称之为解锁。

    死锁:两个或以上的进程,进程之间相互等待对方完成特定任务,造成等待的死循环。

    饥饿:一个可执行的进程长期无法获取到CPU的使用权。 

    信号量:有些进程只需要访问共享资源的某个属性,与其他进程访问该共享资源的的内容并不冲突。如果采用加锁的方式,那么会将降低其他任务的执行效率。信号量是比锁 粒度更小的资源保护机制,常用于共享资源集合的互斥访问。

    原子操作:指整个操作过程不允许发生中断、上下文切换,而是一口气执行完的一系列操作。

   管程:比信号量更高的抽象。它是包含成员变量和函数操作的结构体。它的成员包含1个锁用来指定临界区,0或多个条件变量当作信号量来使用。管程利用一组“信号量”来管理共享资源集合的互斥访问

   上下文切换的时机具有不确定性,在1个CPU的情况下,两个线程同时执行相同的代码。如果没有一个合理的同步、互斥机制可能会得到不同的结果。

    

目录

一、临界区的同步和互斥机制

1)禁用硬件中断

2)两个线程间的临界区访问——Dekker算法

3)N线程间的临界区访问——Bakery 算法

4)基于锁的原子操作Test and set 方法

4)基于锁的原子操作Exchange方法

二、基于信号量的同步互斥机制

1)生产者和消费者

2)信号量的实现

三、管程

1)条件变量的实现

2)生产者和消费者

3)Hansen Style 和 Hoare Style


一、临界区的同步和互斥机制

     临界区

1)禁用硬件中断

     此方法采用屏蔽硬件中断的方法,让临界区的代码不会被中断,不会被上下文切换,自然也就没有了并发问题。但这只适用于单CPU的系统。而对于多CPU的系统还是无法解决互斥问题的。

2)两个线程间的临界区访问——Dekker算法

   此算法需要两个共享的全局变量 turn 和flag 数组,分别表示临界区该被哪一个线程访问  和 表示 哪一个线程想要访问临界区,需给予默认值。其中 i 表示自己的线程标识,j表示另外一个线程的标识。这个算法的原理是:当有两个线程需要访问临界区时,各自发出访问请求 flag[i] = ture ,turn相当于临界区属性,只有进入临界区才可以修改turn标志。而允许进入临界区的条件是当turn的值为自身编号,并且flag [i]为ture时才行。

 

3)N线程间的临界区访问——Bakery 算法

     当由N个进程进入临界区时,每个进程接收到一个数字,得到数字最小的进程进入临界区,如果拿到的数字相同,则进程号pid小的先进入临界区去执行代码。

4)基于锁的原子操作Test and set 方法

    利用锁访问的方式。其lock的属性value默认值为0,代表未被锁。有原子方法TestAndSet  方法,这方法执行时不允许被中断。当请求锁方法种,执行testAndSet,如果发现返回值是0,代表可访问,在TestAndSet 方法种自动将value设置成1,任务执行完后把value重新置成0。当任务在执行过程中,其他进程也需要访问锁时,testAndSet返回值为1,进入忙等循环或者阻塞。

4)基于锁的原子操作Exchange方法

     利用锁和两个内存单元来完成临界区的访问。这两个内存单元的值分别是0 和1。有原子方法Exchange方法,这方法执行时不允许被中断。其原理是利用两个内存单元lock 和 key,lock是全局变量,Key是局部变量,当需要访问时将Key设置成1,用Exchange 的方式来交换lock值,来代表获取锁资源。执行完后再将锁资源置成0.

二、基于信号量的同步互斥机制

   信号量常用于共享资源集合的访问。 信号量的操作只有两种,V()方法代表信号量增加,释放资源。P()方法代表信号量减少,获取资源,当无资源可获取时阻塞线程。信号量是个有符号的整数,是全局变量,初始化值通常等于可访问资源集合的资源数。当信号量的取值范围要求在[0,1]范围内,相当于锁。

1)生产者和消费者

     使可访问的共享资源数增多的任务称为生产者,使可访问的共享资源数减少的任务称为消费者。对于锁的应用,刚开始可访问资源为1,只能允许有1个消费者,当消费资源后,由自身去释放资源,所以自身又是生产者。又例如有一群任务A专门生成文件,又有一群任务B专门读取并删除文件,这个也是生产者消费者模式。这个模式要求:生产者可以多个,当缓存区满时不允许再生产,消费者可以多个,当缓存区空时不再允许消费。缓存区在同一个时刻最多只能一个线程去访问、操作。

    mutex信号量初始值1,用来实现控制缓存区的访问互斥。 fullBuffers 用来控制消费者的可消费资源数。emptyBuffers 用来控制生产者的可生产资源数。

其中缓存区的互斥访问P必须放在最后,由于P操作会造成线程阻塞,如果信号量P()操作顺序不正确,可能会发生死锁。通常的原则是访问越早的资源,其信号量应越晚去P(),由于任何一个任务要先访问缓存区,再去访问缓存区中的资源,所以缓存区自身的信号量mutex 要比 emptyBuffers 和 fullBuffers更晚去 P().

例如将Deposit(c)的emptyBuffers->P() 和 mutex->P()的顺序进行交换。  当emptyBuffers 值为0,FullBuffer值为n 时,生产者A执行Deposit 执行mutex->P()   和emptyBuffers->P()造成阻塞,另消费者B执行FullBuffers->P() 和mutex->P() 也造成阻塞,相互等待对方任务执行V()操作造成死锁。

例如将Deposit(c)的emptyBuffers->P() 和 mutex->P()的顺序进行交换,也将Remove(c)的fullBuffers->P() 和 mutex->P()的顺序进行交换。 当emptyBuffers 值为0,FullBuffer值为n 时,生产者A执行Deposit 执行mutex->P()   和emptyBuffers->P()造成阻塞, 消费者B执行mutex->P() 也造成阻塞,相互等待对方任务执行V()操作造成死锁。

2)信号量的实现

     所需要实现的方法是P() 和V()操作,这两个操作方法是原子指令且必须禁用中断。需要考虑的实现内容是:如何阻塞线程,又如何唤起线程。通常需要一个等待队列用来存 因信号量不足而阻塞的线程。

当sem为负数时代表被阻塞的线程数,sem>=0 时表示可访问资源数。 P操作将信号量减1,并判断原先的资源数是否大于0,如果原先的资源数不足就将此线程加入(等待)阻塞队列。 V操作将信号量加1,并判断原先的资源数是否大于0,如果原先的资源数不足就把刚释放出的资源提供给被阻塞的线程。用队列进行管理阻塞态的线程,先被阻塞,先获取资源。

三、管程

   管程中包含锁,锁用来控制线程的互斥访问,即在某一时刻管程最多只能被一个线程访问。用队列entryQueue来管理待访问管程的线程。管程中包含0个或多个条件变量。这些条件变量相当于信号量集合,用来共同控制共享资源的同步访问。每个信号量利用队列来保存因资源不满足而被阻塞的线程。

1)条件变量的实现

     条件变量类似于信号量它的实现逻辑和信号量类似。但是也有所区别,其中信号量的int类型变量用来记录剩余资源数。而管程条件变量用于记录阻塞线程数,所以当操作时与信号量的操作顺序相反。

条件变量调用wait方法时,让阻塞线程数加1后把线程加入阻塞队列,释放管程的锁并sechedule()阻塞线程让出CPU使用权,阻塞完线程后再获取锁,不让其他外部线程访问管程。条件变量之所以先释放锁后获取锁,原因是在入wait方法之前管程就已经被锁上了。让出CPU使用权时,必须释放锁。不然阻塞线程由于资源不足无法唤醒,锁没释放导致外部线程无法访问管程,那么管程将会无法运行。

条件变量调用signal方法时,如果当前有阻塞线程,把阻塞线程唤醒,阻塞线程数减1.

2)生产者和消费者

       count 用来表示共享资源个数,notFull为可生产剩余资源数的条件变量,notEmpty为可消费剩余资源数的条件变量。

       当线程需要生产资源时,必须先请求锁,如果缓存区则进入阻塞态让出CPU使用权,停止执行该线程的代码。如果该线程被唤醒进入就绪态,重新获取CPU使用权,那么将继续执行原先Condition::Wait方法中的require(lock)。如果当前资源数还是的那么循环继续阻塞,如果没满就生产出了一个单位的资源,唤醒需要消费该资源的阻塞线程。

       当线程需要消费资源时,必须先请求锁,如果缓存区则进入阻塞态让出CPU使用权,停止执行该线程的代码。如果该线程被唤醒进入就绪态,重新获取CPU使用权,那么将继续执行原先Condition::Wait方法中的require(lock)。如果当前资源数还是的那么循环继续阻塞,如果没空就消费掉了一个单位的资源。唤醒需要生产该资源的阻塞线程。

3)Hansen Style 和 Hoare Style

    Hansen和Hoare 是计算机科学家,各提出了一个方法用来解决管程的问题:当管程中执行的线程A发出Signal操作后,意味这需要该资源的阻塞线程B会被唤醒。那么此时A和B线程都在管程中处于就绪态,那么A和B都有可能在管程中执行,这就不满足互斥条件了。

    Hoare提出的思想是在Signal()的时候唤醒因资源不足被阻塞的线程让它先执行,即在资源紧张的情况下,产出资源将立马被消费。。 而Hansen提出:当A发出signal操作时,唤醒等待(阻塞)线程B,先执行完自身A后再并让B执行完。文章上面举的例子都是Hansen模式。 Hansen 和 Hoare在signal()方法上有所区别。 Hansen的Signal一口气执行完。而 Hoare的Signal唤醒阻塞线程后阻塞自己,释放锁让出CPU使用权

   Hoare模式生产者消费者问题:

       当线程需要生产资源时,必须先请求锁,如果缓存区满则进入阻塞态让出CPU使用权,停止执行该线程的代码。如果有消费者signal(),则该线程被唤醒进入就绪态,重新获取CPU使用权,那么将生产出了一个单位的资源,唤醒需要消费该资源的阻塞线程,阻塞自己并交出自己的CPU使用权,等待消费者线程唤醒自己,此时CPU重新回到自己手里意味着执行完毕。

       当线程需要消费资源时,必须先请求锁,如果缓存区空则进入阻塞态让出CPU使用权,停止执行该线程的代码。如果有生产者signal(),则该线程被唤醒进入就绪态,重新获取CPU使用权,那么将消费掉了一个单位的资源。唤醒需要生产该资源的阻塞线程。阻塞自己并交出自己的CPU使用权,等待生产者线程唤醒自己,此时CPU重新回到自己手里意味着执行完毕。

猜你喜欢

转载自blog.csdn.net/superSmart_Dong/article/details/116668370