进程的同步与互斥、常见的同步问题
基本概念
- 临界资源:一次仅允许一个进程使用的共享资源,也就是互斥资源
- 临界区:程序中访问临界资源的那段代码,也称危险区、敏感区
- 互斥:多个程序片段,同一时刻仅有一个能进入临界区
- 同步:若干程序片断运行必须严格按照规定的某种先后次序来运行。同步是一种更复杂的互斥:互斥不会限制程序对资源的访问顺序,即访问是无序的;而同步必须要按照某种次序来运行
临界区管理原则
- 任何两个进程不能同时处于其临界区
- 不应对 CPU 的速度和数量作任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无限等待进入临界区
当我们实现一个锁的时候,需要满足上述 4 个条件,才是一个正确的锁。
信号量和 P / V 操作
信号量是一种特殊的变量,对它的操作都是原子的。对信号量的操作分为 down 和 up,分别对应 P 操作和 V 操作。P、V 来自于荷兰语:Probeer (try)、Verhoog (increment)。V 操作会增加信号量 S 的数值,P 操作会减少它。
- 执行
down操作时,如果信号量值为 0,会使这个进程睡眠,否则,将信号量减 1 - 执行
up操作时,如果这个信号量上有正在睡眠的进程,就唤醒它,信号量值不变;否则,将信号量加 1
如果信号量只有二进制的 0 或 1,称为二进制信号量(binary semaphore)。二进制信号量可以用来实现一个互斥锁(Mutex)。
常见的同步问题及其伪码实现
(1) 生产者与消费者问题
也称有限缓冲问题。生产者和消费者共享固定大小的缓冲区,生产者向缓冲区写入数据,消费者从缓冲区读出数据,生产者不能在缓冲区满时加入数据,消费者也不能在缓冲区空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。消费者同理。
这里如果直接使用操作系统提供的 sleep() 和 wakeup(),可能发生死锁,原因在于:「判断是否要休眠(缓冲区空/满)」和「执行 sleep()」不是一个原子操作,有可能在执行 sleep() 之前被切换到另一个程序,导致 wakeup() 信号丢失,最后两者都进入休眠(见维基百科-生产者消费者问题)。
使用信号量可以解决上述问题。只有 down 操作会使进程休眠,所以要分别使用两个信号量:
- 空槽数
empty:empty为 0 时消费者进入休眠,不再消费 - 满槽数
full:full为 0 时生产者进入休眠,不再生产
在多个生产者和消费者的情况下,有可能出现两个或以上的进程同时读或写同一个缓冲区槽的情况,因此再用一个二值信号量 mutex 实现一个锁。
完整代码如下:
1 |
|
(2) 读者写者问题
- 允许多个进程同时读数据库
- 有进程在读数据库的时候,不允许写数据库
- 如果有一个进程正在写数据库,则不允许其他任何进程访问数据库
1 | typedef int semaphore; // 定义信号量这个数据类型 |
Personal Notes
(3) 浴室洗澡问题
一个浴室,当有一个女生在浴室里,其他女生可以进入,但是男生不行,反之亦然。
1 | typedef int semaphore; |
(4) 哲学家就餐问题
假设有五位哲学家围坐在一张圆形餐桌旁,吃饭或者思考。每位哲学家之间各有一根筷子,哲学家必须用两根筷子吃东西。他们只能使用自己左右手边的那两根筷子。
可能有死锁的写法:每个哲学家都拿着左边的筷子,永远都在等右边的筷子。
1 | typedef int semaphore; |
至少一个哲学家的获取筷子的顺序要与其他人不同,以打破环路等待条件:
1 | void getforks() { |




