10.1 信号量概念
信号量(Semaphore)是另一种临界区的保护机制,它是操作系统提供的一种协调共享资源访问的方法。
它将资源纳入全局考虑,从操作系统的层面对资源进行宏观的调配。
这个机制由Dijkstra在20世纪60年代提出,是早期操作系统的主要同步机制。
信号量Semphore中的整数sem就是这个系统资源剩余量。申请时减少,释放时增加即可,没有时等待分配,所以先进先出。
实现的结构和lock很像。
具体的实现接口为P()
和V()
,分别是荷兰语(?)增加prolagg和减少verhoog的缩写。
信号量的实现(其中sem是剩余资源)
class Semaphore {
int sem;
WaitQueue q;
}
Semaphore::P(){
sem--;
if (sem < 0) {
Add thread t to q;
block(p);
}
}
Semaphore::V(){
sem++;
if (sem <= 0) { // 说明队列中有线程在等待
Remove a thread t from q;
Wakeup(t);
}
}
如果弄懂了clock的机制,从上面的代码中信号量应当是非常好理解的,这个机制拥有如下的特征:
- sem是由操作系统全局保护的整数变量,信号量的相关操作是原子操作。不像软件方法,相关操作不会被打断。
- P()可能阻塞,V()不会阻塞
- 通常假定信号量是“公平的”:
- 线程不会无限地阻塞在P()操作
- 假定信号量等待按先进先出
10.2 信号量的使用
信号量可分为二进制信号量(资源数目为0或1)和资源信号量(资源数目为任何非负值),基于其一可以实现另一个。
信号量可以用于实现临界区的互斥访问和线程间的条件同步。
用信号量实现临界区的互斥访问,示例如下:
mutex = new Semaphore(1);
mutex->P();
Critical Section;
mutex->V();
必须成对使用P()操作和V()操作,不能颠倒、重复或遗漏
利用P和V配对的性质,可以将P和V分开,从而实现条件同步。
在如下图的线程A中,如果调用P,那么信号量将变为-1,进入等待状态(这是信号量设计中很精髓的FIFO)。线程B调用V之后,信号量变为0,这时A重新开始运行,从而就实现了两个线程的同步。
生产者-消费者问题
- 任何时候只能有一个线程操作缓冲区(互斥访问)
- 缓冲区空时,消费者必须等待生产者(条件同步)
- 缓冲区满时,生产者必须等待消费者(条件同步)
用信号量描述在这个问题当中的约束:
- 二进制信号量mutex表示I/O是否被占用,初始化为1
- 资源信号量fullBuffers表示当前占满的缓冲区数量,用于限制读出,初始化为0
- 资源信号量emptyBuffers表示当前为空的缓冲区数量,用于限制写入
伪代码如下:
class BoundedBuffer{
mutex = new Semaphore(1);
fullBuffers = new Semaphore(0);
emptyBuffers = new Semaphore(n);
}
其生产、消费两个过程相互对称,
BoundedBuffer::Deposit(c){
emptyBuffers->P();//条件同步
mutex->P(); //互斥访问
Add c to buffer; //核心操作
mutex->V(); //互斥访问
fullBuffers->V();//条件同步
}
BoundedBuffer::Withdraw(c){
fullBuffers->P();//条件同步
mutex->P(); //互斥访问
Add c to buffer; //核心操作
mutex->V(); //互斥访问
emptyBuffers->V();//条件同步
}
使用信号量的困难在于:
- 读/开发都比较困难
- 容易出错
- 不能系统性地解决死锁问题
10.3 管程
管程是一种更为现代的实现互斥访问的方法。与临界区相比,管程有更好的封装,如果设计得当,则处理各种问题时,使用将极为便捷。但我感觉这可能是迄今为止最难以理解的一节。
类似我们要为临界区实现提供lock和semaphore两种工具类,对于管程,我们还要实现一个条件变量condition类。
管程通过使用条件变量提供对同步的支持,这些条件变量包含在管程中,并且只有管程才能访问
与信号量的一个最为重要的不同在于,使用条件变量时,如果进入等待状态,还能暂时放弃管程的互斥访问等待事件出现时恢复,从而并行进行其他操作,而不是简单地像对临界区那样“固守”。
这一点是使用了条件变量的好处,可以看到中途释放了lock。
用管程解决生产者-消费者问题
通过良好的设计封装,可以降低使用过程的使用难度。
具体使用情景下,Deposit和Remove都只需要简单的对外接口,但有着有效的互斥访问机制。
管程中的条件变量按照不同的释放处理方式分为两种:
- 内部的线程优先执行(Hoare)
- 正占用管程处于执行状态的线程优先执行(Hansen)
具体的实现,按照老师的说法是将管程中的while非常关键,它保证能在管程T2退出之后再恢复T1的管程执行,这样实现的Hansen管程,切换次数较少,实际使用效率更高。
Hoare管程只需要将while改成if。正确性更容易说明,更适合理论分析和教学。(srds我还是不懂
10.4 经典同步问题
哲学家就餐问题(五人和五个叉子交替围圈,每个人有思考吃饭两个状态,吃饭需要左右两只叉子)
要防止死锁,又要有尽可能多的人吃到。
一个合理的思路是,避免大家完全用同样的手先发。
#define N 5
semaphore fork[5];
void philosopher(int i){
while (true)
{
think();
if (i % 2)
{
p(fork[i]);
p(fork[(i+1)%N]);
} else {
p(fork[(i+1)%N]);
p(fork[i]);
}
eat();
V(fork[i]); // 释放不阻塞,因而不用使用分支区别对待
V(fork[(i+1)%N]);
}
}
更详细的解析,见https://blog.csdn.net/theLostLamb/article/details/80741319
读者-写者问题
正是之前所提到的同步互斥的一个抽象版本。要求读读不互斥,读写和写写都互斥。管程可以简化同步问题的实现方法。
这里略去,可见https://blog.csdn.net/hhypractise/article/details/107150434
10.5 练习题
一个进程由阻塞队列进入就绪队列,可能发生了哪种情况_________(A)
- A. 一个进程释放一种资源
- B. 系统新创建了一个进程
- C. 一个进程从就绪队列进入阻塞队列
- D. 一个在阻塞队列中的进程被系统取消了
如果有5个进程共享同一程序段,每次允许3个进程进入该程序段,若用PV操作作为同步机制则信号量S为-1时表示什么_________(C)
- A. 有四个进程进入了该程序段
- B. 有一个进程在等待
- C. 有三个进程进入了程序段,有一个进程在等待
- D. 有一个进程进入了该程序段,其余四个进程在等待
管程的主要特点有__________(ABD)
- A. 局部数据变量只能被管程的过程访问
- B. 一个进程通过调用管程的一个过程进入管程
- C. 不会出现死锁
- D. 在任何时候,只能有一个进程在管程中执行