Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

读者与写者

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

复习

  • 等待与通知:条件变量如何让线程在条件不满足时睡眠
  • 生产者与消费者:用互斥和等待通知串起一个完整例子
  • 信号量:用计数管理有限资源与线程协作

TL;DR

  • 读操作之间可以同时进行,写操作必须独占
  • 读多写少时,让多个读者并发能大幅提高效率
  • 用“读者计数”控制:第一个读者加锁,最后一个读者解锁
  • 要防止写者被源源不断的读者饿死

正文

  有些共享数据,被“读”的次数远多于被“写”。比如一本配置表、一份缓存。如果还像之前那样,读的时候也独占着锁,就太浪费了——好几个人一起看,本来相安无事。

三条规则

  读者与写者问题,规则其实很自然:

  • 读—读:可以并发,多个读者同时读没问题
  • 读—写:互斥,有人在读时不能写,有人在写时不能读
  • 写—写:互斥,同一时刻只能有一个写者

  最难的是前两条之间的协调:什么时候允许一堆读者进来,什么时候又必须拦住新读者、把机会让给写者。

读者计数

  一个经典做法是用一个读者计数

  • 第一个进场的读者负责 加锁,把写者挡在外面
  • 最后一个离场的读者负责 解锁,放写者进来
  • 中间进出的读者,只增减计数,不去碰锁
读者:
  lock(计数保护锁)
  计数 += 1
  if (计数 == 1) lock(数据锁)   // 第一个读者上锁
  unlock(计数保护锁)
  读数据
  lock(计数保护锁)
  计数 -= 1
  if (计数 == 0) unlock(数据锁) // 最后一个读者解锁
  unlock(计数保护锁)

写者:
  lock(数据锁)
  写数据
  unlock(数据锁)

  这样一来,只要还有读者在场,写者就得等;而读者之间可以畅通无阻。

公平性问题

  上面这个版本有个隐患:写者可能被饿死。只要读者络绎不绝,计数永远回不到 0,写者就一直等下去。

  这就引出了读者优先写者优先两种策略:

  • 读者优先:读者随时可以进,写者可能要等很久
  • 写者优先:一旦有写者等待,就拦住新来的读者,先让写者写完

  选哪种,要看场景:配置表这种“几乎不写”的数据,读者优先更划算;而一旦有写就必须尽快生效的数据,则需要写者优先,避免更新迟迟落不了地。

  无论哪种,都要小心别把另一方饿死——这再次说明,同步问题里没有免费的午餐,只有权衡。

思考题

  如果只有“读者优先”而没有写者优先,一个持续不断的读取请求流会给写操作带来什么后果?现实中的系统可能怎样缓解?

小结

知识点

  • 读读可并发,读写、写写互斥
  • 读者计数:第一个读者加锁,最后一个解锁
  • 写者可能被读者饿死
  • 读者优先与写者优先的取舍

参考资料

  1. Wikipedia(zh):读者写者问题:readers-writers
  2. Wikipedia(zh):读写锁:read-write lock

思考题答案(仅供参考)

  在读者优先下,源源不断的读请求会让读者计数一直不为 0,写者的锁始终等不到,更新就一直被推迟——写者被“饿死”。现实中常用的缓解办法是:一旦有写者在等待,就暂时拦住后到的读者,让写者先完成(即写者优先);或者限制连续读者的数量,给写者留出窗口。可见,多一类角色参与竞争,就多一层公平性的权衡。

协议

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

封面图

设计师 | 南国微雪