想象一下,公司茶水间里放着一本“公共设计笔记”。任何人路过都可以翻开查阅(读),负责人也可以在上面修订(写)。如果两个人同时改同一页,笔记会乱成一团;如果有人正在改、另一个人凑过来看,看到的就是写到一半的草稿。但如果是几个人一起看同一页,完全没问题。问题来了:怎么设计一条规矩,让“看书的人互不打扰、改书的人独占书房”,同时不让某类人永远轮不到机会?这就是操作系统里的“读者写者问题”,它是并发编程中最经典的入门模型之一。

岗位关联

读者写者问题是理解并发、锁与数据库一致性的底层思维模型。掌握它,你才能在设计 AI 服务的高并发数据层时,既保证读吞吐,又避免脏读与写者饥饿——这是后端、数据库和 AI 基础设施工程师的核心基本功。

目录
  1. 可视化演示
  2. 问题本质刻画
  3. 读者优先策略
  4. 写者优先策略
  5. 公平策略设计
  6. 读写锁实现要点
  7. 性能与权衡
  8. 实战应用落地
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

交互式演示读者写者问题的三种解决方案:读者优先、写者优先和公平调度,展示多线程并发访问共享资源时的同步机制和饥饿问题规避。

本节配有交互动效(进入视口后自动加载,约 3KB)。

公平调度策略 读者和写者按到达顺序依次获取锁 多个读者可同时读取共享资源 写者独占访问,确保数据一致性 避免饥饿:每个线程最终都能获得执行机会

问题本质刻画

茶水间里的那本公共笔记,本质上就是操作系统里的“共享数据”:可能是一条数据库记录、一个缓存键、一个文件段落。来翻阅笔记的人叫“读者”,它们的操作是读取,不改变内容;来修订笔记的人叫“写者”,它们的操作是修改,会留下痕迹。读者彼此之间完全可以挤在同一页前各看各的,但写者动手时必须拥有一个绝对安静的环境,否则任何凑过来看的读者,都会看到“改了一半”的半成品。

操作系统把这种场景抽象成三个必须同时满足的约束:多个读者可以同时访问共享数据;写者在修改时,任何读者和其他写者都不能打扰它;进程不能因为长期轮不到机会而被饿死。前两条保证了数据的正确性——也就是“不会读到中间状态”,第三条保证了系统的活性——也就是“不会有人永远等着”。

用形式化语言来描述,就是三种访问组合中,只有一种被允许并发:

所以管理员的职责就是一句话:放读者们一起进书房,而写者必须单独进场。理解了这三条规则,就拿到了后面所有调度策略的钥匙。

{Ri∥Rj: 允许Ri∥Wj: 禁止Wi∥Wj: 禁止\displaystyle\begin{cases}R_i \parallel R_j:\ \text{允许}\\[4pt]R_i \parallel W_j:\ \text{禁止}\\[4pt]W_i \parallel W_j:\ \text{禁止}\end{cases}
公式说明:三个进程两两相遇的组合里,只有“读者-读者”允许同时进入临界区;“读者-写者”与“写者-写者”都必须串行执行。
  1. 读者-读者组合两个只读进程 i、j 可以同时访问同一份数据,符号 \parallel 表示两者并发执行、互不等待。
    Ri∥Rj: 允许R_i \parallel R_j:\ \text{允许}
  2. 读者-写者组合只要有写者正在修改数据,读者必须等待,避免读到修改中途的中间状态。
    Ri∥Wj: 禁止R_i \parallel W_j:\ \text{禁止}
  3. 写者-写者组合两个写者更不能同时修改数据,否则最终内容由谁说了算都说不清楚。
    Wi∥Wj: 禁止W_i \parallel W_j:\ \text{禁止}

读者优先策略

新来的这位管理员是个“讨好多数派”的人:只要有人在看书,他就不断放新读者进去,因为放读者进去总没错;只有房间彻底空了,他才让等了一下午的写者进场。这套做法就是读者优先(Readers-Preference)——读者的感受永远排第一,写者只能见缝插针。

把它翻译成调度逻辑:只要当前还有读者在访问共享数据,后续读者不必等待、直接进入;写者则必须等到读者计数归零才能进门。实现时维护一个读者计数 R(readcount):新读者到达时先检查当前的读者数——只要有人正在读,就放心进去;至于写者,门口竖着一把“写者锁”,谁都不能在有读者的情况下拿到它。

读者优先的准入条件可以浓缩成一句话:读者任何时候都能进,写者只有在没人读时才能进。

读者像潮水一样一波波涌进来,写者只能在水完全退去的间隙进场;如果潮水永不退去,写者就只能永远站在门口——这就是“写者饥饿”。读者优先把吞吐做到了极致,却把公平性的风险全部推给了写者。

读者进入条件: R≥0;写者进入条件: R=0\displaystyle\text{读者进入条件}:\ R \ge 0;\qquad\text{写者进入条件}:\ R = 0
公式说明:读者只看读者计数是否不小于 0——由于 R 天然不小于 0,等于无条件放行;写者则必须等到读者计数归零,也就是房间里空无一人。
  1. 读者计数 RR 表示当前正在房里阅读的读者人数,它只能是非负整数。
    R∈{0,1,2,… }R \in \{0, 1, 2, \dots\}
  2. 读者进入新读者到达时只要 R ≥ 0 即可进场,等于不加任何限制地放行。
    读者进入: R≥0\text{读者进入}:\ R \ge 0
  3. 写者进入写者要等所有读者离开,也就是必须满足 R = 0。
    写者进入: R=0\text{写者进入}:\ R = 0

写者优先策略

换了位雷厉风行的管理员:只要有人登记要修订笔记,他立刻在门口挂上“暂停入内”的牌子,新来的读者都在门外排队,保证写者尽快拿到空房间。这就是写者优先(Writers-Preference)——把被亏待的一方翻身扶正。

写者优先的规则是:一旦有写者在等待,之后到达的读者必须排队阻塞,不能插队进房。注意一个细节:已经在房内的读者允许读完再走,强行清场会把正在进行的读操作撕成碎片。实现时用“等待写者数 W”登记:写者到达时 W 加 1;只要 W > 0,新读者就被挡在门外;当房间空出时,优先叫醒写者进场。

对比读者优先,写者优先把“谁说了算”的权力反转了过来:读者进门要看写者脸色,写者进门只看房间空不空。

写者被“插队保护”了,但普通读者可能遇到黑压压的写者队伍,一整天进不了书房。读者饥饿同样真实存在——可见“优先”这个词,永远是把一方的幸福建立在另一方的等待之上。

读者进入条件: W=0;写者进入条件: R=0\displaystyle\text{读者进入条件}:\ W = 0;\qquad\text{写者进入条件}:\ R = 0
公式说明:读者进门必须确认没有写者在等待(W=0);写者进门则只需确认房间已空(R=0),门外排多少读者都与它无关。
  1. 等待写者计数 WW 表示已登记等待进场的写者数量,它同样是非负整数。
    W=0 或 W>0W = 0\ \text{或}\ W > 0
  2. 读者进入条件只要 W > 0,后来的读者就要在门外排队,防止读者潮继续淹没写者。
    读者进入时: W=0\text{读者进入时}:\ W = 0
  3. 写者进入条件写者进场只关心房内读者数是否归零,不需要向读者队伍低头。
    写者进入时: R=0\text{写者进入时}:\ R = 0

公平策略设计

为了避免两拨人互掐,管理员在门口装了个取号机:先来后到,叫号进场。谁先到谁拿号,想插队门都没有,读者写者一视同仁。这就是公平策略(Fair Scheduling)的直观画面,它的核心只有四个字:先来后到。

公平策略要解决的是“饥饿”这个结构性缺陷。它让所有请求按到达顺序排队:当一个写者排在队伍前面时,之后到达的读者必须排在它后面,不能抢先进入房间;反过来也一样,一个排在写者前面的读者,写者也不能越过它。这样每一方都有机会,谁都不会被永远晾在门外。

工程实现上,通常会维护一个 FIFO 队列或“等待写者计数”:有写者在等待时,新读者不再直接进场,而是排进队列。这种策略的代价是调度逻辑变复杂,每次读者进门都要多检查一次等待状态,因此公平是用一点点吞吐换来的。

取号机的意义不是让所有人都变快,而是让“永远轮不到”这件事从根源上消失。公平性不能提高吞吐,但它治愈了饥饿——这也是操作系统调度里“活性”概念的真正含义。

等待时间≤(前方请求数+1)×T最长占用\displaystyle\text{等待时间} \le (\text{前方请求数} + 1) \times T_{\text{最长占用}}
公式说明:只要请求按序号排队,任何一个请求的等待时间都有上界——它由前方排队数量和每次读写的最大占用时间决定,不会无限增长。
  1. 排队位置每个请求按到达顺序拿到一个队列位置,前方请求数记为 n,它是个非负整数。
    n∈{0,1,2,… }n \in \{0, 1, 2, \dots\}
  2. 单次占用上限每次读或写操作都有最长占用时间 T,取读者和写者中的较大者作为上界。
    T=max⁡{T读, T写}T = \max\{T_{\text{读}},\ T_{\text{写}}\}
  3. 等待有上界最多等完前方 n 个请求、自己再占用一次,总等待时间被限制在有限范围内。
    等待时间≤(n+1)×T\text{等待时间} \le (n + 1) \times T

读写锁实现要点

书房的“门禁系统”由两个零件组成:读门是一个计数器,显示当前有几个人在看书;写门是一把只能由一个人握住的钥匙。计数器归零的那一刻,钥匙才会被交给等着的写者。这套门禁搬进操作系统,就是读写锁(Read-Write Lock)。

经典实现里需要两把锁:一把“写者锁”来控制写者的独占,一把“互斥锁”来保护读者计数器不被多个读者同时改写。读者进房的动作是:先用互斥锁护住计数器,把读者数加 1;如果自己是第一位读者,就把写者锁锁上,挡住可能的写者;然后释放互斥锁,安心进房。读者离房时反向操作:读者数减 1,如果自己是最后一位,就释放写者锁。

写者就简单得多:直接去拿写者锁,拿到就进房,离开时释放。整个实现浓缩成一个精妙的分工——首读者上锁,末读者解锁。

计数器让“很多人同时看”成为可能,写者锁让“没人看时才能改”成为铁律,两个零件缺一不可。这道门禁,就是所有读者写者策略在地基处的共同蓝图。

首位读者: 获取L写;末位读者: 释放L写;写者: 获取并释放L写\displaystyle\text{首位读者}:\ \text{获取} L_{\text{写}};\qquad\text{末位读者}:\ \text{释放} L_{\text{写}};\qquad\text{写者}:\ \text{获取并释放} L_{\text{写}}
公式说明:写者锁由“第一位进门的读者”抢到、“最后一位出门的读者”释放;写者每次进出都要完整持有一次写者锁。这样读者之间共享同一把锁,写者则独占这把锁。
  1. 首读者上锁当读者数从 0 变成 1,也就是首位读者进场时,必须锁住写者锁,宣告“现在开始不许写”。
    readcount=1⇒获取L写\text{readcount} = 1 \Rightarrow \text{获取} L_{\text{写}}
  2. 末读者解锁当读者数从 1 变成 0,也就是最后一位读者离开时,释放写者锁,宣告“房间空了,写者可以进”。
    readcount=0⇒释放L写\text{readcount} = 0 \Rightarrow \text{释放} L_{\text{写}}
  3. 计数保护读者数的加减操作要用互斥锁保护,避免两个读者同时修改计数器导致计数错乱。
    Δreadcount 受 L互斥 保护\Delta \text{readcount}\ \text{受}\ L_{\text{互斥}}\ \text{保护}

性能与权衡

读者优先时,书房利用率最高,因为读者们能挤在一起看;写者优先时,修订从不被耽误,但读者被挡门外时房间常常空着没人用。管理员渐渐意识到,怎么调和这个两难,取决于来看书的人里改稿的占多少——这就是性能权衡的本质。

性能对比有两个维度:吞吐量是单位时间完成多少次访问,延迟与饥饿则关心某个请求要等多久。读写锁和普通互斥锁的关键差异,在于读者能否并行。假设有 N 个读者、M 个写者,每次读耗时 t\_r、写耗时 t\_w。普通互斥锁把所有操作串行化,总耗时是两者的线性叠加;读写锁允许最多 P 个读者同时读数,读者部分的耗时被压缩到原来的 1/P。

两个方案的总耗时之比,就是读写锁带来的“加速比”:

所谓“选策略”就是选性价比:读者占绝对多数时,让读者扎堆最划算;写者频繁时,再多门禁也省不下时间,不如用一把简单的锁。读多写少四个字,是读者写者问题从理论走向工程的第一条准绳。

\displaystyleS = \frac{N \cdot t_r + M \cdot t_w}{\dfrac{N}{P}\cdot t_r + M \cdot t_w}
公式说明:分子是普通互斥锁的串行总耗时,分母是读写锁的并行总耗时,两者相除得到加速比。读者越多、写者越少、并行度 P 越大,收益越明显。
  1. 串行耗时普通互斥锁下,N 个读者和 M 个写者依次执行,总耗时是两者耗时之和。
    T互斥=N⋅tr+M⋅twT_{\text{互斥}} = N \cdot t_r + M \cdot t_w
  2. 并行耗时读写锁下读者最多 P 人同时执行,读者耗时降为 N/P 份,写者依旧串行。
    T读写锁=NP⋅tr+M⋅twT_{\text{读写锁}} = \frac{N}{P} \cdot t_r + M \cdot t_w
  3. 加速比两个总耗时相除,得到读写锁相对互斥锁的性能倍数。
    S=T互斥T读写锁S = \frac{T_{\text{互斥}}}{T_{\text{读写锁}}}

实战应用落地

书房的经验出了公司依然好用:数据库就是一本巨大的公共笔记,缓存是贴在门口的小抄。工程上最常见的场景,正是“很多人看同一份数据,少数人改它”,读者写者模型几乎原封不动地搬进了数据库和缓存的架构里。

第一个场景是数据库读写分离:写请求只能进主库,主库是唯一的写者;读请求可以打向多个从库,从库们就是并发读者。主从同步时,从库短暂读到旧数据是可以容忍的“滞后窗口”,但主库的写入必须串行,这正是写者独占的体现。第二个场景是缓存更新:更新数据库后删掉缓存,下一次读会 miss,再去数据库拉新值回填。第三个场景是框架内建的读写锁,比如 Go 的 sync.RWMutex、Java 的 ReentrantReadWriteLock,把门禁系统做成了开箱即用的零件。

其中最容易踩坑的是缓存更新的顺序。正确做法是“先更新数据库,再删除缓存”;如果反过来“先删缓存、再更新数据库”,中间一个极短的间隙里,另一个线程可能用旧值回填缓存,造成长期脏读。

写者改笔记时先把门口小抄撕掉,读者下次重新进来看新版——这就是缓存世界里“写者优先但不允许读者读到半成品”的朴素智慧。从原理到实战,读者写者问题从未走远。

正确: Update DB→Delete Cache→读新值回填;错误: Delete Cache→Update DB→旧值回填\displaystyle\text{正确}:\ \text{Update DB} \to \text{Delete Cache} \to \text{读新值回填};\qquad\text{错误}:\ \text{Delete Cache} \to \text{Update DB} \to \text{旧值回填}
公式说明:两个序列的差别在于“删缓存”与“改数据库”谁先谁后。先删后改会让写者尚未完成时读者就把旧值“钉”回缓存,等于读者看到了半成品。
  1. 先更库后删缓存数据库是唯一真相源,改完库删掉缓存,迫使后续读者重新从数据库拉取新值。
    Update DB→Delete Cache\text{Update DB} \to \text{Delete Cache}
  2. 先删缓存后更库删除后、改库前,读者读到了旧库值并重新填回缓存,旧值被长期钉在缓存里。
    Delete Cache→Update DB⇒旧值回填\text{Delete Cache} \to \text{Update DB} \Rightarrow \text{旧值回填}
  3. 一致性窗口先更库后删缓存仍有极短的不一致窗口,可通过延迟双删或版本号校验进一步收窄。
    窗口→0 当采用版本校验\text{窗口} \to 0\ \text{当采用版本校验}

AI 应用场景

数据库读写分离架构

阶段:数据存取与后端服务设计

解决的问题:AI 服务启动时高频加载特征、推理时反复读取模型配置,但配置更新一天只有几次。如果所有读写都挤在单库上排队,QPS 上不去。用读者写者模型:写请求只进主库(写者独占),读请求负载均衡到多个从库(读者并发),读并发度按从库数量成倍放大。

具体案例:某 AI 推荐系统的特征存储,线上每秒上千次特征读取,配置更新只有个位数。团队搭建一主两从,写走主库,读走从库,读 QPS 提升近 50 倍,写者更新时也不再影响主力读流量。

缓存更新策略(Cache Aside)

阶段:模型推理与缓存层

解决的问题:模型参数或特征版本更新时,缓存里的旧值会让线上拿到滞后的推理结果,相当于读者读到了“改了一半的笔记”。用读者写者模型的顺序思想:先更新数据库(写者独占),再删除缓存,让下一次读强制 miss 并回填新值。

具体案例:一个 NLP 服务的词表每周更新一次,更新时先改数据库版本号、再删缓存并延迟双删,保证线上推理在同一版本窗口内读取到一致的词表。

推理服务中的模型热更新

阶段:多线程推理服务

解决的问题:模型权重热更新时,正在推理的请求可能读到“换了一半”的权重矩阵。用 Go 的 sync.RWMutex 实现读写锁:推理请求持读锁并发执行,模型更新持写锁等待,更新完成后新请求才能读到新权重。

具体案例:某在线翻译服务的模型每天灰度更新多次,借助 RWMutex 读多写少的特性,推理延迟抖动小于 5%,同时彻底消除了“半模型权重”导致的乱码输出。

Python 演示

python87 行
import threadingimport timeimport random  class ReadWriteLock:    """带策略选项的读写锁, reader_priority=True 表示读者优先"""     def __init__(self, reader_priority=True):        self.cond = threading.Condition()        self.readers = 0               # 当前读者数 R        self.writer = False            # 是否有写者在工作        self.writers_waiting = 0       # 等待中的写者数 W        self.reader_priority = reader_priority     def r_lock(self):        with self.cond:            while self.writer or ((not self.reader_priority) and self.writers_waiting > 0):                self.cond.wait()       # 写者优先时, 新读者给写者让路            self.readers += 1     def r_unlock(self):        with self.cond:            self.readers -= 1            if self.readers == 0:                self.cond.notify_all()  # 最后一位读者唤醒所有等待者     def w_lock(self):        with self.cond:            self.writers_waiting += 1            while self.writer or self.readers > 0:                self.cond.wait()       # 写者必须等所有读者离场            self.writers_waiting -= 1            self.writer = True         # 宣示独占, 防止被唤醒的读者抢跑     def w_unlock(self):        with self.cond:            self.writer = False            self.cond.notify_all()     # 写者离开, 唤醒所有人  def reader_work(lock, name, records):    t0 = time.perf_counter()    lock.r_lock()    t1 = time.perf_counter()    records.append((name, 'read', t0, t1))    time.sleep(random.uniform(0.02, 0.05))    lock.r_unlock()  def writer_work(lock, name, records):    t0 = time.perf_counter()    lock.w_lock()    t1 = time.perf_counter()    records.append((name, 'write', t0, t1))    time.sleep(random.uniform(0.06, 0.1))    lock.w_unlock()  def simulate(reader_priority):    lock = ReadWriteLock(reader_priority)    records = []    jobs = [('R1', reader_work), ('R2', reader_work), ('R3', reader_work),            ('W1', writer_work), ('R4', reader_work), ('W2', writer_work)]    random.shuffle(jobs)    threads = []    for name, work in jobs:        t = threading.Thread(target=work, args=(lock, name, records))        threads.append(t)        t.start()        time.sleep(0.01)               # 错开到达时间, 模拟随机到达    for t in threads:        t.join()    return records  def main():    for priority in (True, False):        records = simulate(priority)        print('reader_priority =', priority)        for name, op, t0, t1 in sorted(records, key=lambda x: x[2]):            print('  %s  %s, 等待 %.3f 秒' % (name, op, t1 - t0))        print()  if __name__ == '__main__':    main()
目的:用线程和条件变量实现一个带策略开关的读写锁,并在读者优先与写者优先两种模式下模拟“多读者 + 多写者”随机到达,打印每个请求从发起到进入资源区的等待时间。
  1. while self.writer or self.readers > 0:写者等待的条件:有写者正在干活或还有读者在场,都不得进入临界区
  2. self.writer = True用布尔标志宣示“我进来了”,防止被唤醒的读者抢跑造成写读重叠
  3. if self.readers == 0: self.cond.notify_all()最后一位读者离场时叫醒所有等待者,把门禁交给写者
  4. while self.writer or ((not self.reader_priority) and self.writers_waiting > 0):读者让路的条件:有写者在工作;写者优先模式下还有写者排队时读者也等待
  5. threading.Thread(target=work, args=(lock, name, records))每个读者/写者是一个线程,模拟真实程序里的并发访问
  6. time.sleep(0.01)错开线程启动时间,模拟请求随机到达,让“写者被读者潮挡住”的现象可见

常见误区

误区“读操作之间本来就不冲突,给读和写各上一把普通锁,写的时候锁写锁、读的时候锁读锁,各管各的不就完事了?”

正解两把互相独立的普通锁做不到“读者互不影响但写者一票否决”。读锁和写锁互不干涉,写进程想拦读进程根本拦不住,必须靠同一个共享门禁来协调。真正需要的是读写锁:读者之间共享同一把读锁,写者独占同一把写锁,靠“首读者上锁、末读者解锁”把两者的边界缝合起来。

为何常见:把“互斥”误解成“每个角色一把锁”,忽略了读与写之间也必须互斥这个核心约束。

误区“读者优先嘛,写者最多就是多排几分钟队,怎么会真的永远进不去?最多算延迟高一点。”

正解如果读者到达的速率始终高于读者离开的速率,读者数永远不会降到 0,写者的等待时间就没有上界——这不是“久一点”,而是数学上的无限等待。就像门口总有人插进来看书,管理员永远等不到房间空下来的那一秒。解决思路是改用写者优先或公平队列,给写者的等待设一个上限。

为何常见:生活中排队总能等到,但并发系统里“持续到达”可以无限期推迟资源释放,饥饿是真实发生在极端流量下的场景。

误区“既然是优先,把写者放前面,系统响应肯定更快,性能自然更好。”

正解写者优先只改善写者的延迟,代价是阻塞后续读者:房间明明空着,读者却必须在门外等写者先执行,吞吐被白白浪费。性能要用吞吐量和延迟两个指标综合衡量,写者优先的适用场景是“写者很少但每次写很关键”,比如配置更新,而不是通用的性能优化手段。

为何常见:把“优先”和“更快”画等号,忽略了优先背后的资源分配其实是有代价的——你给一方开绿道,就是在给另一方设红灯。

误区“读写锁听起来这么厉害,只要读多写少就无脑上读写锁,肯定不会错。”

正解读写锁有额外的计数器维护和条件判断开销。如果临界区极小(比如只加一个计数),普通互斥锁反而更快——读写锁的读者每次进出都要更新 readcount 并检查“是否首位/末位”,这些操作本身就是成本。实践上应该用加速比公式估算并行收益,再实测对比,而不是凭直觉选型。

为何常见:技术选型只看理论收益,忽略了锁实现本身的 CPU 开销与竞争成本;现实中“最优”永远依赖具体场景的量化测量。

面试题

Q1. 在读者写者问题中,关于“读”与“写”的关系,下列说法正确的是?

单选题 · easy

  • A.多个读者必须排队依次读
  • B.读者与写者可以同时进行
  • C.多个读者可同时读,写者必须独占← 正确答案
  • D.写者与写者之间可以同时进行

答案:C。读者只读不改,互相不冲突,可以并发;写者会改动数据,与任何读者或其他写者都冲突,必须独占。就像公共笔记可以几个人同看一页,但修改时必须清场。

Q2. 读者优先策略的核心调度规则是?

单选题 · easy

  • A.一旦有写者等待,新读者必须排队
  • B.只要有读者在读数,新读者即可进入,写者等到读者数为零← 正确答案
  • C.读写严格按到达顺序交替执行
  • D.写者永远比读者先执行

答案:B。读者优先的核心是“读者不等写者,写者等读者”:只要房内还有读者,新读者就进;读者数为 0 时写者才进门。选项 A 是写者优先或公平策略的做法。

Q3. 读者优先策略最可能引发的问题是?

单选题 · mid

  • A.读者饥饿
  • B.写者饥饿← 正确答案
  • C.必然死锁
  • D.数据一定不一致

答案:B。若读者持续到达,写者永远等不到 R=0 的时刻,等待时间无上界,即写者饥饿。死锁不是必然结果,数据一致性由写者锁的互斥保证,问题只出现在公平性层面。

Q4. 写者优先策略中,新到达的读者在什么情况下会被阻塞?

单选题 · mid

  • A.只要有读者在房内
  • B.只要房间空闲
  • C.只要有写者正在等待← 正确答案
  • D.读者永远不被阻塞

答案:C。写者优先让新读者给等待中的写者让路,即 W>0 时读者排队,避免读者潮继续淹没写者。已经在房内的读者不受影响,可以读完再走。

Q5. 公平策略解决饥饿问题的关键机制是?

单选题 · mid

  • A.让写者永远优先
  • B.让读者永远优先
  • C.按到达顺序排队,有写者等待时新读者排在其后← 正确答案
  • D.随机选择下一个进入者

答案:C。公平策略本质是 FIFO 加等待写者计数:请求按到达顺序排号,写者排在前面时后来的读者不能插队,保证每个请求的等待时间有上界,从而消除饥饿。

Q6. 经典读者写者实现中,互斥锁(mutex)的作用是?

单选题 · hard

  • A.保证读者之间不能同时读
  • B.保护读者计数器的更新← 正确答案
  • C.让写者优先执行
  • D.加速读操作

答案:B。mutex 保护 readcount 的加一减一操作,防止两个读者同时修改计数器导致计数错误;它让首位读者能安全地获取写者锁、末位读者能安全地释放写者锁。

Q7. 读写锁相比普通互斥锁的最大优势在于?

单选题 · easy

  • A.写操作也能并发执行
  • B.允许读操作并发,提升读多场景的吞吐← 正确答案
  • C.完全消除饥饿
  • D.减少写者的等待时间

答案:B。读写锁把“读”从串行中解放出来,多个读者可并行,读多写少时吞吐显著提升。但写操作依旧独占,公平性由具体调度策略决定,读写锁本身不保证无饥饿。

Q8. 关于各类调度策略,下列说法正确的有?

多选题 · mid

  • A.读者优先可能使写者饥饿← 正确答案
  • B.写者优先可能使读者饥饿← 正确答案
  • C.公平策略保证每个请求等待时间有上界← 正确答案
  • D.公平策略下读者永远不能先于写者执行

答案:ABC。读者优先把写者一直往后推,写者优先把读者往后推,都会造成某一方饥饿;公平策略用 FIFO 保证了有界等待。D 错,公平策略下读者和写者都按到达顺序,先到的读者当然可以先执行。

Q9. 关于读写锁的经典实现,下列说法正确的有?

多选题 · hard

  • A.第一位读者进入时获取写者锁← 正确答案
  • B.最后一位读者离开时释放写者锁← 正确答案
  • C.写者每次进入都获取并释放写者锁← 正确答案
  • D.读者计数器不需要任何保护

答案:ABC。首位读者拿写者锁挡住写者,末位读者放锁放行写者,写者每次进出都完整持有写者锁,这正是经典实现的核心三件事。读者计数器被多个读者并发修改,必须用互斥锁保护,D 错。

Q10. 读者写者问题在实际工程中的应用场景包括?

多选题 · easy

  • A.数据库读写分离← 正确答案
  • B.缓存更新策略← 正确答案
  • C.框架提供的读写锁(如 RWMutex)← 正确答案
  • D.纯 CPU 计算的 SIMD 指令

答案:ABC。数据库读写分离、缓存更新、框架读写锁都是读者写者模型的直接落地;SIMD 属于硬件层面的并行计算,不涉及共享数据的读写同步问题,因此不属于该模型的应用场景。