10-5管程

管程抽象程序比信号量还高。抽象越高,给开发者更加容易的来完成同步的问题,

信号量开始就是操作系统的一个同步机制的实现,

而管程最早提出的时候使用在语言的level,编程语言这个level,这些语言通过设计这个管程机制可以简化高级语言来完成同步互斥操作,并不是一开始就用在操作系统设计的。管程的设计针对语言的并发机制来完成的。

管程:monitor,本意监视器或者监管的意思,管程是包含了一系列共享变量,以及针对这些变量操作的函数的一个组合,一个模块。

管程设计里面包含一个lock,这个lock确保进入所有要访问管程管理的函数,只能有一个线程,所以需要1个lock来保证互斥性。

他需要有0到多个条件变量,因为在这里面需要大量访问各种各样的共享的资源或者共享变量,在访问过程中,有可能条件得不到满足,需要把得不到满足的线程给挂在条件变量上。

通过这两个机制组合就可以实现这个管程。

上图,圆圈中间部分视为管程中的操作函数,整体可以视为临界区。

进入管程需要一个队列,右上角entry queue,进入管程是互斥的,首先要取得这个lock,取得进入,取不到在外面等待队列里面。

进入管程管理空间后呢,就可以执行这个管程维护的一系列函数,可以理解为操作或者函数,就是我们上图中间那一段, 像圆柱体的。在执行函数的过程中,这些函数是对共享变量进行操作,有可能针对某个共享变量,共享资源的操作得不到满足了,他需要等待,需要注意,它是互斥的占用这个管程,所以说,如果这时候它等待的时候,它需要把自身挂到某一个地方去,同时把这个lock给释放掉。才可以让其他的等待这个lock的其他线程能够去执行,那他等在什么地方呢?等在条件变量上,这个条件变量也是管程管理很重要的一部分。

在上图里面是x、y,两个条件变量,它有两个队列,也是等待队列,这个等待队列挂着所有需要等待的一些线程,需要某个条件,x管某个条件,y管某个条件,这个条件满足,它会唤醒相应的线程。

关于条件变量,它的操作也是一样,有两个操作,和我们信号量类似,一个wait一个signal操作。

wait操作让这个线程等在这个条件变量里面

signal操作是说,唤醒下这个条件变量,使得挂在这个上面的线程它有机会去继续执行。

上图是一个大致结构图。

注意上图左边,release lock后 再require lock,和通常使用不一样。先释放管程访问权,让别的线程可以访问管程,然后进行调度,也就是执行别的线程,等回来之后,再次申请管程的锁。

上图右边wakeup是说把当前sleep的线程,重新置成ready状态,那这些线程有可能再次被调用。

这里需要注意一点,如果现在在等待队列里面没有等待的线程,那这个操作啥也不做,这一点是和我们刚刚说的信号量是不一样的,虽然它们都有一个变量,整型变量,信号量里面叫sem,这里叫num waiting,它们的含义很不一样。numwaiting代表当前等待的线程个数,而那个sem代表的是信号量的个数,这两个在处理上也是不一样的,信号量的实现里面,它的P和V是一定要执行的,这里就不一样,wait操作是做加操作,但是signal不一定要做减操作。

用管程来解决生产者消费者问题,如下:

初始化

上图,acquire和release必须在头和尾,这是管程定义里面决定的,定义是说,线程进入到管程的时候,只有一个线程能进,才能执行管程管理的所有函数。

上图左边,count==n,就是buffer满的时候,会做一个notFull.Wait(&lock)操作,notFull是一个条件变量,notFull和信号量不一样,不需要有初始值,notFull.Wait(&lock)表明当前已经满了,我需要睡眠,同时还带一个lock,这个lock就是前面说的lock->Acquire 的lock,就是管程的lock,刚才讲的条件变量实现里面,wait操作里面在schedule之前做了一个release lock的操作,做release lock 实际就是说,让当前这个生产者释放掉这个锁,这使得其它的线程才有可能进入管程去执行,因为这时候这个程序要睡眠了,所以它必须要把这个锁释放,它释放是因为在执行这个函数最开始有个acquire lock,它已经获得了这个锁,所以在wait操作里面一定要把这个锁给释放掉,如果它不释放,它就去睡眠了,导致的后果就是哪些等待进入管程的线程都在那等着,整个系统就停滞了,所以一定要在睡眠之前,在做wait操作的时候一定要去做release lock这么一个操作。一旦将来被唤醒了,也就意味着它从schedule里面可以继续往下执行,那他就再去完成一次lock的acquire,一旦获得这个lock之后,它可以继续的跳出这个wait操作,然后再去做这个while循环,看count是否等于n,这是它一个执行过程。

对应消费者里面有一个notFull的signal操作。buffer满的时候,消费者消费了一个,count做了个减减,这个时候buffer已经不满了,所以这时候应该去做一次提醒,就是notFull的signal操作。一旦notFull里面有等待的线程,就会被唤醒

buffer空的时候,消费者这边也会有一个while循环,判断count==0,如果满足,它会做一个wait,会做一个notEmpty.Wait(&lock)的操作,直到生产者有一个notEnpty.Signal操作后,它才能被唤醒继续执行,这个合在一起,就形成了一个完整的一个用管程来解决生产者和消费者问题的一个实现。

还需要注意一个细节就是,当线程在管程中执行的时候, 如果某个线程要执行这个针对某个变量的signal操作,就是唤醒操作的时候,当执行完这个操作之后,是马上去执行等待在这个条件变量上的那个线程,还是说让发出唤醒操作的这个线程执行完毕之后,再去让那个等待的线程执行?这两者是不一样的。

因为一旦发出signal操作,其实也意味着,当前在这个管程里面,会有两个线程都可以执行,(这个跟定义里面不一样),一个是本身我发出signal操作的线程,它本来就在执行,第二个呢,因为你signal之后,意味着你要把那个等待的线程给唤醒,那唤醒的线程本身它也应该能够执行,这样就两个线程可以执行,到底选择哪个先执行?这是个问题,有两种方法:

1:Hoare提出来的,hoare比较完美的办法,一旦发出signal操作之后,我就应该让等待的线程继续执行,它自身去睡眠,这是一种比较急切的让等待的线程执行,然后直到那个等待的线程执行完毕之后,执行到release之后,它这个发出signal的线程才能够继续执行。

2: Hansen提出的, 当我发出signal操作之后呢,并不意味着我马上要放弃CPU,马上把控制权交给这个等待被唤醒的线程去执行,而是等到这个做signal操作的线程执行完release操作之后,执行完这个lock release操作之后,才把控制权交给那个等待被唤醒的线程去执行。

方法1看上去直观点,但是实现起来比较困难,主要见于教材中

方法2实现起来相对比较简单,在很多操作系统里面就是这么实现的,方法1实现起来需要更复杂的机制才能保证实现方法的有效性。 主要用于真实OS和JAVA中

hoare详细实现流程图:

Hansen详细实现流程图:

这两种方法对我们管程中条件变量的使用也会造成一定的影响,大家看看有什么样的影响,

上图,用hoare的实现方法其实可以把while变成if,为什么有这样的区别?

大家注意下,用while还是if其实是由于刚才那个唤醒机制的实现不同造成的,对于hansen的实现来说,当他做完signal操作之后,它并没有马上去让那个等待的线程被唤醒的那个等待线程去执行,他必须要继续往下执行, 做release才能释放,这种情况下,有可能有多个等待在条件变量上的线程都被唤醒,意味着存在多个被唤醒线程,大家都可能去抢这个继续执行的count,只有一个能够抢到,假如这时候是另外一个生产者进程抢到,使得缓冲区再次满了,所以说有可能被唤醒的那些线程,当他能够被选中去占用CPU执行的时候,count已经不为n了,所以必须要用while来再次做一个确认,是否count==n,这是一个由于hansen实现方法造成的一个结果。

但是用hoare实现,实验机制就更加简单了,因为当你做完signal操作之后,你肯定把这个控制权交给那个被唤醒的等待线程,那这时候只有一个等待线程被唤醒,不会有多个,因为它做完signal之后就唤醒一个,这个线程等到执行,占用CPU执行它的操作,那么他这时候可以继续往下执行,这个时候count一定不为n,因为做signal的时候,它的条件就是说当count

Hansen管程和Hoare管程

管程的实现策略有两种,分别是Hansen管程和Hoare管程,它们的主要区别在于进程调度策略的不同。

假定首先有一个进程进入管程执行管程的例程,它在执行过程中需要等待某个资源,因此该进程进入阻塞状态,并且放弃了CPU的使用权。此后,第二个进程得以进入管程,该进程首先是释放了第一个进程请求的资源,第一个进程因而得以被唤醒。在Hansen管程的语意下,第二个进程将继续执行,直到它因为等待某个资源或者执行完毕,而让出管程的访问权限,此后第一个进程才有可能被调度。

Hensen实现代码里面用的是while,这是因为,在Hansen管程的语意下,被阻塞的进程需要和其他尚未进入管程的进程,同时竞争管程访问的权限mutex。因此,当被阻塞进程被唤醒后,也许有一个另外的生产者进程已经得到执行,使得当前缓冲区再次满了,被唤醒的进程需要再次判断它请求的资源是否为空闲。

而在Hoare管程的语意下,第二个进程释放第一个进程的资源后,将第一个进程唤醒,然后自己立刻进入阻塞状态,此时第一个进程将得到调度执行。该语意的实质是保证已经进入管程的阻塞进程优先于尚未进入管程的进程执行。因此,当第一个进程执行完毕后,它不会释放锁,而是直接将CPU的控制权转交给第二个进程。

Hoare实现代码里面用的是if,在Hoare管程的语意下,wait操作和signal操作都需要做一定的修改。具体说来,在signal操作中,唤醒一个等待进程后,不释放管程锁,将自身加入signal queue中进入等待状态,此时只有被唤醒的进程可以得到管程的使用权。一个进程执行管程例程结束后,首先检查是否有处于signal queue状态的进程,如果有,也不释放管程锁,直接选择其中一个唤醒即可。这是因为进程被唤醒后,一定可以优先得到管程的使用权,因此不需要像上面那样对条件做循环判断。

总结:

在Java中,可以使用`synchronized`关键字实现管程来解决生产者消费者问题。下面是实现的步骤: 1. 定义一个缓冲区,用来存储生产者生产的数据和消费者消费的数据。 2. 定义一个计数器,用来记录缓冲区中的数据个数。 3. 定义生产者和消费者线程,分别负责生产数据和消费数据。 4. 在生产者和消费者线程中,使用`synchronized`关键字来锁住缓冲区,确保线程安全。 5. 生产者在生产数据之前判断缓冲区是否已满,如果已满则等待;如果未满则将数据添加到缓冲区中,并将计数器加一。 6. 消费者在消费数据之前判断缓冲区是否为空,如果为空则等待;如果不为空则从缓冲区中取出数据,并将计数器减一。 下面是一个使用`synchronized`实现生产者消费者问题的示例代码: ``` public class ProducerConsumer { private static final int BUFFER_SIZE = 10; private static int count = 0; private static int[] buffer = new int[BUFFER_SIZE]; public static void main(String[] args) { Thread producer = new Thread(new Producer()); Thread consumer = new Thread(new Consumer()); producer.start(); consumer.start(); } static class Producer implements Runnable { public void run() { while (true) { synchronized (buffer) { if (count == BUFFER_SIZE) { try { buffer.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } buffer[count++] = 1; System.out.println("Producer produced, count=" + count); buffer.notify(); } } } } static class Consumer implements Runnable { public void run() { while (true) { synchronized (buffer) { if (count == 0) { try { buffer.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } buffer[--count] = 0; System.out.println("Consumer consumed, count=" + count); buffer.notify(); } } } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值