一、信号量
之前通过锁机制,实现了在临界区域内进程的互斥执行。但这个是远远不够的,如果想要实现进程间的同步操作或者实现临界区中多个线程的执行,那么就需要高层次的编程抽象和底层硬件支持。
信号量的抽象数据类型:
- 一个整形(sem),两个原子操作
- P(): sem减1,如果sem<0,等待,否则继续
- V(): sem加1,如果sem>=0,唤醒一个等待的P
信号量种类:
- 二进制信号量:实现了lock功能,也就是实现互斥。可以是0或1;也可以实现调度约束:就是一个进程执行到P()进行等待,然后等另外一个进程执行到V()从而唤醒等待的线程,确保同步
- 一般/计数信号量:可以取任何值。一般条件同步(采用调度约束实现一个线程等待另一个线程的事情发生)
信号量的实现
需要借助于等待队列来完成,对于上述的block和wakeup方法可以看作是入队和出队的步骤。
信号量的用途:互斥和条件同步(注意等待的条件是独立的互斥);
信号量的缺点:读/开发代码困难;容易出错(使用的信号量被另一个线程占用,完了释放信号量);不能够处理死锁;
二、管程
一开始存在于语言层面去控制并发问题,后来才应用于操作系统层面。
目的:分离互斥和条件同步的关注
定义:一个锁(临界区)加上0个或若干个条件变量(等待/通知信号量用于管理并发访问的共享数据)
管程实现的一般方法:收集在对象/模块中的相关共享数据;定义方法来访问共享数据。
所有进程在右上角的排队队列中,排队完后进行wait()操作,等到signal()操作唤醒后,执行这个进程的代码。
组成:
- Lock
要么使用锁,对锁进行acquire或者release来抢占或者释放锁
- Contion Variable
要么使用条件变量,如果条件不满足wait,就持续等待,一旦满足条件就唤醒队列中的线程继续执行
条件队列实现
wait方法中,numWaiting是一个统计量,统计了有多少线程处于等待队列中,注意这里必须要在先进行release操作之后,才可以执行睡眠等待,不然会触发死锁。一旦唤醒就会立刻抢占锁。
上述是condition的实现,而在管程中带有两个这样的condition:
对于Deposit操作:
对于buffer没有满的情况这边就不讨论,如果buffer已满也就是count == n 时,就会在notFull的Condtion中将一开始加的锁解掉,那么这个时候deposit的线程会进入到condtion中的schedule进行休眠等待。那么remove操作就可以抢占到锁,然后开始从buffer中进行消费,然后唤醒之前睡眠的线程,最后释放锁。
如果这边remove的线程抢不到锁,那么新的抢占到锁的deposit线程就会有又一次进入到wait操作,再次存放于等待队列并且释放锁后休眠等待。
一旦之前的锁被唤醒就会尝试去获取锁,如果获取到了锁,那么外层的count == n 就不会满足所以就跳出了循环,将线程防止于buffer中,等待消费。变更完统计量后,再次去唤醒相同情况下的notEmpty条件等待队列中的线程,最后再释放自己的锁。