操作系统-同步问题分析
生产者-消费者问题
问题描述:一组生产者和一组消费者共享一个大小为 n 的缓冲区;只有缓冲区还有空位的时候生产者才能往里放数据;只有缓冲区不为空的时候消费者才能从中取数据;缓冲区是临界资源,只允许一个生产者往里放数据或者一个消费者从中取数据
关系分析:因为缓冲区是临界资源,所以需要一个信号量 mutex
控制生产者和生产者、生产者和消费者以及消费者和消费者之间的互斥访问;又因为只有缓冲区还有空位的时候生产者才能放数据进去,所以还需要一个信号量 empty
控制生产和放入数据两个动作的同步关系;同理,还需要信号量 full
控制取出数据和消费之间的同步关系
伪代码:
semaphore mutex = 1, empty = n, full = 0;
// 生产者
producer() {
P(empty);
P(mutex);
do_something;
V(mutex);
V(full);
}
// 消费者
consumer() {
P(full);
P(mutex);
do_something();
V(mutex);
V(empty);
}
为什么我们先做 P(mutex)
而不是 P(empty)
或 P(full)
呢?从我们的定义上理解,mutex
是控制缓冲区(临界资源)访问的信号量,在 P(mutex)
之前我们应该做好所有准备工作,信号量 empty
的控制并不是访问缓冲区过程中的动作,而是控制是否进入缓冲区访问的动作,因此应该在 mutex
之前进行;从反面理解,如果我们把它们的顺序调换,那么显然会出现生产者占据了 mutex
导致消费者在 P(mutex)
中被挂起,无法释放 empty
从而导致死锁的情况。或许可以从谓词逻辑角度做一个形式化的证明?
读者-写者问题
问题描述:有一组读者和一组作者共同对一本书进行阅读和修改,多个读者可以同时阅读这本书;有读者阅读时作者无法修改,作者修改时读者无法阅读;多个作者无法同时进行修改。按存在读者阅读时,作者的修改是否会被无限推迟把这个问题划分为读者优先和作者优先两种。
关系分析:对于读者优先问题:读者和读者之间不存在互斥关系,不需要任何信号量;当存在一个作者时,其他作者和读者不能访问这本书(临界资源),这是一种互斥关系,因此需要一个信号量 writer
;当存在多个读者时,作者是不能修改临界区资源的,因此我们引入一个信号量 reader
,每有一个读者进入就 V,退出就 P,又因为只有当读者的数量为 0 的时候作者才能进行修改,所以获取读者数量是一个很重要的事情,但是 P V 操作不允许我们获取信号量 reader
的大小,所以我们修改原来的设计,把信号量 reader
变为一个计数器 reader
,同时引入一个信号量 mutex
控制多个读者进程对计数器的互斥访问,每有一个读者进程就把计数器加一,只有当现在的读者退出后计数器为 0 才允许作者修改,即 V(writer)
。
对于作者优先问题,当已经有一个作者进程在等待,就不再允许新的读者进程进入了。所以可以再引入一个信号量 exist
控制它。感觉实际上再引入一个计数器和对应的信号量也是可以的,不过不如一个信号量方便。
第一眼看上去读者-写者问题和生产者-消费者很类似,为什么我们把它们划分成两个问题呢?它们的本质区别在于作者进程是在某个值小于等于 0 时才能进行,这与 P V 操作本身的定义不太相合,导致单纯用信号量去解决读者和写者之间的互斥关系非常困难。所以在这个问题中,我们引入一个计数器 count
。感觉我的描述还不是很清楚。
补充一点,P 和 V 对于某个信号量一定是成对出现的,可以用来检查算法。
伪代码:
// 读者优先
int count = 0;
semaphore mutex = 1, writer = 1;
reader() {
P(mutex);
if (count == 0) {
P(writer);
}
count++;
V(mutex);
do_something();
P(mutex);
count--;
if (count = 0) {
V(writer);
}
V(mutex);
}
writer() {
P(writer);
do_something();
V(writer);
}
// 作者优先
int count = 0;
semaphore mutex = 1, writer = 1, exist = 1;
reader() {
P(exist);
P(mutex);
if (count == 0) {
P(writer);
}
count++;
V(mutex);
V(exist);
do_something();
P(mutex);
count--;
if (count = 0) {
V(writer);
}
V(mutex);
}
writer() {
P(exist);
P(writer);
do_something();
V(writer);
V(exist);
}
哲学家进餐问题
问题描述:一个圆桌上五名哲学家,每两名哲学家之间有一根筷子,设计算法让哲学家能够轮流进餐。
关系分析:筷子是互斥资源,但是如果对每根筷子设置一个信号量很容易导致死锁状态;因此,“一根筷子”并不是互斥资源,“一对筷子”才是互斥资源。增加一个信号量 mutex
使得每个哲学家对左右两根筷子的操作是互斥的。mutex
可以只有一个,使得所有哲学家依次地拿起两双筷子,也可以有 5 个,代表 1 2, 2 3, 3 4, 4 5, 5 1 五对筷子。
也可以用 AND 信号量解决这个问题
伪代码:
semaphore mutex = 1, chopstick[5] = [1, 1, 1, 1, 1];
philosopher() {
P(mutex);
P(chopstick[i]);
P(chopstick[i + 1]);
V(mutex);
do_something();
V(chopstick[i]);
V(chopstick[i + 1]);
}
考研习题分析
生产者-消费者模型的变体,使用信号量 mutex
维护缓冲区的互斥使用,使用信号量 empty
维护空缓冲区,使用信号量 odd
维护缓冲区中奇数,使用信号量 even
维护缓冲区中偶数
semaphore mutex = 1, empty = N, odd = 0, even = 0;
P1() {
P(empty);
P(mutex);
number = produce();
put(number);
V(mutex);
if (number % 2) V(odd) else V(even);
}
P2() {
P(odd);
P(mutex);
getodd();
V(mutex);
V(empty);
countodd();
}
P3() {
P(even);
P(mutex);
geteven();
V(mutex);
V(empty);
counteven();
}
还是生产者-消费者的变体,使用信号量 mutex
控制取号机的互斥使用,使用信号量 empty
和 full
维护顾客数量,使用信号量 service
控制营业员提供服务
semaphore mutex = 1, empty = 10, full = 0, service = 1;
customer() {
P(empty);
P(mutex);
取号机获取号码;
V(mutex);
V(full);
P(service);
等待叫号;
接受服务;
}
staff() {
P(full);
V(empty);
叫号;
V(service);
提供服务;
}
生产者-消费者问题的变体,特点是初值为 x 或 y
semaphore A_full = x, A_empty = M - x, B_full = y, B_empty = N - y, A_mutex = 1, B_mutex = 1;
哲学家问题的变体,在原问题上添加了一个互斥资源碗,为了防止有一双筷子的哲学家在等待有碗的哲学家,有碗的哲学家在等待有一双筷子的哲学家导致死锁,必须把碗和一双筷子作为一个互斥资源来处理
semaphore chopsticks[n] = [1, 1, ..., 1], bowls = m, mutex = 1;
philosopher() {
P(mutex);
P(chopsticks[i]);
P(chopsticks[i+1]);
P(bowls);
V(mutex);
do_something();
P(mutex);
V(chopsticks[i]);
V(chopsticks[i+1]);
V(bowls);
V(mutex);
}
答案给出了另外一种思路,也行