读者与写者
本章为进阶内容,零基础读者可以跳过,不影响后续阅读。
复习
- 等待与通知:条件变量如何让线程在条件不满足时睡眠
- 生产者与消费者:用互斥和等待通知串起一个完整例子
- 信号量:用计数管理有限资源与线程协作
TL;DR
- 读操作之间可以同时进行,写操作必须独占
- 读多写少时,让多个读者并发能大幅提高效率
- 用“读者计数”控制:第一个读者加锁,最后一个读者解锁
- 要防止写者被源源不断的读者饿死
正文
有些共享数据,被“读”的次数远多于被“写”。比如一本配置表、一份缓存。如果还像之前那样,读的时候也独占着锁,就太浪费了——好几个人一起看,本来相安无事。
三条规则
读者与写者问题,规则其实很自然:
- 读—读:可以并发,多个读者同时读没问题
- 读—写:互斥,有人在读时不能写,有人在写时不能读
- 写—写:互斥,同一时刻只能有一个写者
最难的是前两条之间的协调:什么时候允许一堆读者进来,什么时候又必须拦住新读者、把机会让给写者。
读者计数
一个经典做法是用一个读者计数:
- 第一个进场的读者负责 加锁,把写者挡在外面
- 最后一个离场的读者负责 解锁,放写者进来
- 中间进出的读者,只增减计数,不去碰锁
读者:
lock(计数保护锁)
计数 += 1
if (计数 == 1) lock(数据锁) // 第一个读者上锁
unlock(计数保护锁)
读数据
lock(计数保护锁)
计数 -= 1
if (计数 == 0) unlock(数据锁) // 最后一个读者解锁
unlock(计数保护锁)
写者:
lock(数据锁)
写数据
unlock(数据锁)
这样一来,只要还有读者在场,写者就得等;而读者之间可以畅通无阻。
公平性问题
上面这个版本有个隐患:写者可能被饿死。只要读者络绎不绝,计数永远回不到 0,写者就一直等下去。
这就引出了读者优先与写者优先两种策略:
- 读者优先:读者随时可以进,写者可能要等很久
- 写者优先:一旦有写者等待,就拦住新来的读者,先让写者写完
选哪种,要看场景:配置表这种“几乎不写”的数据,读者优先更划算;而一旦有写就必须尽快生效的数据,则需要写者优先,避免更新迟迟落不了地。
无论哪种,都要小心别把另一方饿死——这再次说明,同步问题里没有免费的午餐,只有权衡。
思考题
如果只有“读者优先”而没有写者优先,一个持续不断的读取请求流会给写操作带来什么后果?现实中的系统可能怎样缓解?
小结
知识点
- 读读可并发,读写、写写互斥
- 读者计数:第一个读者加锁,最后一个解锁
- 写者可能被读者饿死
- 读者优先与写者优先的取舍
参考资料
- Wikipedia(zh):读者写者问题:readers-writers
- Wikipedia(zh):读写锁:read-write lock
思考题答案(仅供参考)
在读者优先下,源源不断的读请求会让读者计数一直不为 0,写者的锁始终等不到,更新就一直被推迟——写者被“饿死”。现实中常用的缓解办法是:一旦有写者在等待,就暂时拦住后到的读者,让写者先完成(即写者优先);或者限制连续读者的数量,给写者留出窗口。可见,多一类角色参与竞争,就多一层公平性的权衡。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪