Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

生产者与消费者

复习

  • 互斥锁:让同一时刻只有一个执行线进入临界区
  • 自旋锁与睡眠锁:等待锁时应该原地重试还是让出 CPU
  • 等待与通知:条件变量如何让线程在条件不满足时睡眠

TL;DR

  • 生产者往缓冲区放数据,消费者从缓冲区取数据
  • 缓冲区空时,消费者要等;缓冲区满时,生产者要等
  • 用一把互斥锁加两个条件变量,就能完整实现
  • 这是一个把互斥、等待、通知串起来的经典例子

正文

  前面几章的工具,单看都简单。把它们凑到一块儿,看一个真正有用的例子:生产者与消费者

场景

  一批线程叫生产者,负责往一个有限大小的缓冲区里放数据;另一批线程叫消费者,负责从缓冲区里取走数据。

  这里有两条难以回避的约束:

  • 缓冲区满了,生产者不能再放,得等
  • 缓冲区空了,消费者不能取,也得等

  同时,缓冲区是共享的,任何一次读写都必须互斥——否则又会撞上“程序竞争”。

三样工具

  把需求对号入座,正好用上前面三样工具:

  • 互斥锁:保护缓冲区,保证同一时刻只有一个线程动它
  • 条件变量 notEmpty:缓冲区不空时,通知消费者
  • 条件变量 notFull:缓冲区不满时,通知生产者

伪代码

  消费者的逻辑:

lock()
while (缓冲区为空) {
    wait(notEmpty, 锁)     // 睡下并放开锁,等生产者放数据
}
取出一个数据
signal(notFull)            // 告诉生产者:有空位了
unlock()

  生产者的逻辑正好对称:

lock()
while (缓冲区已满) {
    wait(notFull, 锁)      // 睡下并放开锁,等消费者腾出空位
}
放入一个数据
signal(notEmpty)           // 告诉消费者:有数据了
unlock()

三个要点

  这段代码虽短,却把并发里的三个关键词都用到了:

  1. 互斥lock / unlock 保证缓冲区同一时刻只被一个人动
  2. 等待:条件不满足时,wait 睡下并释放锁,不占着茅坑
  3. 通知:一方改变了条件,就 signal 唤醒另一方

  尤其注意:等待时必须把锁放开。否则,消费者攥着锁睡下,生产者就永远进不来、放不了数据,大家全卡死。条件变量的 wait 帮你把“放开锁”这件事一并做了。

  这个例子也说明:同步工具往往要组合使用。锁管互斥,条件变量管等待与通知,单靠哪一个都不够。

思考题

  如果消费者在 wait 时不释放锁,程序会怎样?为什么“等待”和“持有锁”不能同时进行?

小结

知识点

  • 生产者放数据、消费者取数据,缓冲区有大小限制
  • 空要等、满要等,读写还要互斥
  • 用互斥锁加两个条件变量实现
  • 等待时必须释放锁,避免死锁

参考资料

  1. Wikipedia(zh):生产者消费者问题:producer-consumer
  2. Wikipedia(zh):条件变量:等待与通知

思考题答案(仅供参考)

  如果 wait 不释放锁,消费者就会“攥着锁睡觉”:它等的是生产者放数据,而生产者要拿的正是它手里的锁,于是永远拿不到,也就永远放不了数据,双方僵死。所以“等待”和“持有锁”必须分开——wait 睡下的同时把锁放开,让别人能进来改变条件,才有机会把自己唤醒。这也正是条件变量必须和锁配套使用的原因。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪