目录
本系列博客是操作系统课堂学习和阅读Allen B. Downey的 The Little Book of Semaphores一书的学习笔记。
本篇分享信号量题目中最经典的生产者-消费者问题,侧重个人的理解,如有错漏欢迎指出。本文伪代码对于信号量的操作形式是
P
/
V
,并且不严格区分进程和线程。
生产者-消费者问题(producer-consumer problem)
应用:事件驱动程序(event-driven programs)。如需要响应的鼠标事件、磁盘中到达的数据流、网络上传输的包等等。
无限缓冲区生产者-消费者问题(infinite buffer producer-consumer problem)
- 描述:有生产者、消费者两种线程,生产者不定时生产产品(
produce()
)放入(put()
)缓冲区,消费者负责从缓冲区取走(get()
)产品并加工(process()
)。假定缓冲区可以放任意数量的产品。要求:- 缓冲区同时只能由一个线程访问;
- 缓冲区没有产品时,消费者线程阻塞。
- 分析:
- 互斥关系:缓冲区一次只能一个生产者或消费者访问;
- 同步关系:若缓冲区空,则消费者等待生产者;
- 变量设置:为了保证对缓冲区的互斥访问,需要设置一个
mutex
信号量;生产者到消费者需要使用一个信号量products
进行同步。
- 伪代码:
Semaphore mutex = 1 Semaphore products = 0 //生产者解法1(在mutex中释放信号): while(1) { produce() //生产产品 P(mutex) put() //放入产品 V(products) //释放信号 V(mutex) } //生产者解法2(在mutex外释放信号): while(1) { produce() //生产产品 P(mutex) put() //放入产品 V(mutex) V(products) //释放信号 } //消费者: while(1) { P(products) //等待信号 P(mutex) get() //取出产品 V(mutex) process() //加工产品 }
- 理解与讨论:
-
本题的生产者线程有两种正确写法,其中第二种的执行效率更高。因为它在已经释放了
mutex
之后才释放信号量,使得被唤醒的消费者线程能够直接获得mutex
;而第一种解法的消费者可能被唤醒之后不能立刻获得mutex
又被挂起,造成不必要的线程切换开销。一个原则是:在没有危险的情况下,尽量将语句放在临界区外执行; -
一个经典的会造成死锁的错误解法是:
//消费者: P(mutex) //先进行P(mutex)操作 P(products) get() V(mutex) process()
对于这种典型的“保持并等待”造成的死锁,Downey的评价是:
" …any time you wait for a semaphore while holding a mutex, there is a danger of deadlock. When you arechecking a solution to a synchronization problem, you should check for this kind of deadlock."
在类似的连续对两个不同信号量进行
P
操作时,原则是:先对用于同步的信号量进行P
操作,再对用于互斥的mutex
信号量进行P
操作。
-
有限缓冲区生产者-消费者问题(finite buffer producer-consumer problem)
- 描述:在上述一般的生产者、消费者问题的基础上,限制缓冲区最多只能放置N个产品。要求:如果缓冲区满,则生产者线程阻塞。
- 分析:
- 互斥关系:不变;
- 同步关系:新增:若缓冲区满,则生产者需要等待消费者 ;
- 变量设置:额外设置一个
spaces
信号量用于消费者到生产者的同步。
- 伪代码(采用在
mutex
外释放信号的方式):Semaphore mutex = 1 Semaphore product = 0 Semaphore spaces = N //生产者线程: while(1) { produce() P(spaces) P(mutex) put() V(mutex) V(products) } //消费者线程: while(1) { P(product) P(mutex) get() V(mutex) V(spaces) process() }
- 理解与讨论:
- 可以看出,本题具有高度的对称性:消费者要等待生产者生产出产品,而生产者也可能需要等待消费者“生产”出空间。可以理解为一个双向的无限缓冲区生产者-消费者问题;
- 初始化时
product + spaces == N
这个等式一定成立,但实际执行时不一定。
缓冲区大小为1的生产者-消费者问题
- 要求:上述有限缓冲区问题的特例:缓冲区的大小为1。
- 分析:同步互斥情况与上一题相同,但
mutex
可省略,如下: - 伪代码:
Semaphore product = 0 Semaphore space = 1 //生产者线程: while(1) { produce() P(space) put() V(product) } //消费者线程: while(1) { P(product) get() V(space) process() }
- 理解与讨论:
mutex
可以省略的原因是:缓冲区只能放1个产品的情况下,如果我们保证了生产者和消费者满足双向的同步关系,其实就已经顺带满足了缓冲区互斥条件。可以说,product
和space
这两个信号量各自承担了一部分原先mutex
的职责;- 生产者会在
space
上排队,消费者会在product
上排队,因此将其分别命名为producerQueue
和consumerQueue
也很贴切。一个生产者通过producerQueue
之后,会把producerQueue
锁上,并把consumerQueue
解锁,让一个消费者通过;一个消费者通过consumerQueue
之后,会把consumerQueue
锁上,并把producerQueue
解锁——这就像一个“单刀双掷开关”,任何时候只允许一方通过,并且通过之后立刻改变掷向。
参考资料:
The Little Book of Semaphores (Version 2.2.1), Allen B. Downey, 2016
缓冲区大小为1的生产者-消费者问题:https://www.cnblogs.com/lca1826/p/6589296.html