读者-写者问题
字数 2328 2025-11-02 13:21:23
读者-写者问题
问题描述
读者-写者问题(Reader-Writer Problem)是操作系统中一个经典的并发同步问题。它模拟了一种场景:多个进程(或线程)并发地访问一个共享数据区(如文件、数据库等),这些进程被分为两类:
- 读者(Reader):只读取数据,不会修改数据。
- 写者(Writer):会写入(修改)数据。
问题的核心在于需要保证同步,以满足以下要求:
- 正确性要求:
- 写写互斥:任意两个写者不能同时访问共享数据。
- 读写互斥:一个写者和一个读者也不能同时访问共享数据。
- 性能要求:
- 读读并发:多个读者可以同时访问共享数据,因为这不会导致数据不一致。
- 公平性:应避免读者或写者发生“饥饿”(Starvation)现象。例如,如果一直有新的读者到来,可能会导致写者长时间无法执行。
解题过程(解决方案的演进)
我们将从最简单的方案开始,逐步优化,最终实现一个公平的解决方案。
第一步:第一类读者-写者问题(读者优先)
这种方案优先考虑读者的并发性,但可能导致写者饥饿。
-
核心思路:
- 使用一个互斥锁(
mutex)来保护一个计数器(read_count)。 - 使用一个写锁(
wrt)来实现写写互斥和读写互斥。 - 计数器
read_count用来记录当前正在读取的读者数量。
- 使用一个互斥锁(
-
需要的变量:
int read_count = 0;(当前活跃的读者数)semaphore mutex = 1;(用于保护read_count的互斥锁)semaphore wrt = 1;(用于写者之间、读者与写者之间的互斥锁)
-
算法流程:
写者进程:
do { wait(wrt); // 申请写锁 // ... 执行写操作 ... signal(wrt); // 释放写锁 } while (true);- 写者很简单,在写之前必须先独占
wrt锁。只要持有这个锁,其他写者和读者都无法进入临界区。
读者进程:
do { wait(mutex); // 申请修改 read_count 的权限 read_count++; // 增加读者计数 if (read_count == 1) { // 如果我是第一个读者 wait(wrt); // 我需要去申请写锁,目的是为了阻塞写者 } signal(mutex); // 释放修改 read_count 的权限,允许其他读者进来修改计数 // ... 执行读操作 ... // 多个读者可以同时在此处读 wait(mutex); // 申请修改 read_count 的权限 read_count--; // 减少读者计数 if (read_count == 0) { // 如果我是最后一个离开的读者 signal(wrt); // 释放写锁,允许写者进入 } signal(mutex); // 释放修改 read_count 的权限 } while (true); - 写者很简单,在写之前必须先独占
-
工作原理解析:
- 第一个读者:当第一个读者到来时,它会获取
wrt锁。这之后到来的所有写者都会被阻塞在wait(wrt)上。 - 后续读者:后续的读者在第一个读者之后到来,它们只会增加
read_count,而不会再去尝试获取wrt锁(因为if (read_count == 1)条件不成立),因此它们可以立即开始读操作。这样就实现了“读读并发”。 - 读者离开:每个读者离开时都会减少计数。只有当最后一个读者离开时,才会释放
wrt锁。 - 写者饥饿:如果读者源源不断地到来(即在前一批读者全部离开之前,总有新的读者到达),那么
read_count永远不会降为0,wrt锁也就永远不会被释放,导致写者无限期等待(饥饿)。
- 第一个读者:当第一个读者到来时,它会获取
第二步:第二类读者-写者问题(写者优先)
这种方案旨在提高写者的优先级,避免写者饥饿。当有写者在等待时,新到达的读者必须等待。
-
核心思路:
- 在“读者优先”的基础上,增加一个信号量(
read_try)和一个计数器(write_count)及其保护锁。 read_try信号量用于实现“写者优先”的规则:当有写者准备写时,它会阻止新的读者进入队列。
- 在“读者优先”的基础上,增加一个信号量(
-
需要的变量(在第一步基础上增加):
int write_count = 0;(等待或正在写的写者数)semaphore mutex2 = 1;(用于保护write_count)semaphore read_try = 1;(实现“写者优先”的关键信号量)
-
算法流程:
写者进程:
do { wait(mutex2); // 保护 write_count write_count++; if (write_count == 1) { wait(read_try); // 第一个写者阻止新读者 } signal(mutex2); wait(wrt); // 申请写锁 // ... 执行写操作 ... signal(wrt); // 释放写锁 wait(mutex2); write_count--; if (write_count == 0) { signal(read_try); // 没有写者了,允许新读者 } signal(mutex2); } while (true);读者进程:
do { wait(read_try); // 尝试读,但可能被写者阻塞 wait(mutex); read_count++; if (read_count == 1) { wait(wrt); } signal(mutex); signal(read_try); // 一旦进入准备读的状态,就释放 read_try // ... 执行读操作 ... wait(mutex); read_count--; if (read_count == 0) { signal(wrt); } signal(mutex); } while (true); -
工作原理解析:
- 写者到来:第一个写者会获取
read_try锁。这之后新来的读者会被阻塞在wait(read_try),无法进入排队序列,从而实现了写者优先。 - 现有读者:在写者到来之前已经开始读的读者,或者已经通过
wait(read_try)的读者,可以继续执行。但新的读者必须等待。 - 这解决了写者饥饿问题,但可能会导致读者饥饿(如果写者源源不断)。
- 写者到来:第一个写者会获取
第三步:公平的读者-写者问题
最公平的解决方案是遵循“先来先服务”原则,无论是读者还是写者。这可以通过一个额外的互斥信号量来实现。
-
核心思路:
- 使用一个全局的排队信号量(
queue)。所有进程(读者和写者)在尝试访问共享数据前,都必须先在这个信号量上排队。 - 这个信号量不关心你是读者还是写者,只保证申请的先后顺序。
- 使用一个全局的排队信号量(
-
需要的变量:
semaphore queue = 1;(全局排队锁)semaphore wrt = 1;(写锁)int read_count = 0;semaphore mutex = 1;(保护read_count)
-
算法流程:
写者进程:
do { wait(queue); // 进入全局队列 wait(wrt); // 申请写锁 signal(queue); // 释放队列锁,允许后面的人排队 // ... 执行写操作 ... signal(wrt); // 释放写锁 } while (true);读者进程:
do { wait(queue); // 进入全局队列 wait(mutex); read_count++; if (read_count == 1) { wait(wrt); // 第一个读者申请写锁 } signal(mutex); signal(queue); // 释放队列锁,允许后面的人排队 // ... 执行读操作 ... wait(mutex); read_count--; if (read_count == 0) { signal(wrt); // 最后一个读者释放写锁 } signal(mutex); } while (true); -
工作原理解析:
- 公平性:
queue信号量形成了一个“走廊”。无论是读者还是写者,都必须先在这个“走廊”排队。这就保证了严格的先来先服务。 - 例如,如果执行顺序是
读者A -> 写者B -> 读者C:- 读者A先拿到
queue锁,然后拿到wrt锁,之后释放queue锁,开始读。 - 写者B到达,它拿到了释放后的
queue锁,但随后尝试获取wrt锁时被阻塞(因为读者A正拿着),于是写者B在queue锁后等待。 - 读者C到达,它尝试获取
queue锁,但此时锁在写者B手里,所以读者C被阻塞在queue上。 - 读者A读完,释放
wrt锁。 - 被阻塞的写者B获得
wrt锁,执行写操作,写完后再释放wrt和queue锁。 - 读者C终于拿到
queue锁,然后开始它的流程。
- 读者A先拿到
- 这种方式有效地防止了任何一方的饥饿,实现了公平访问。
- 公平性: