# 同步,第 7 部分:读者编写器问题
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-7%3A-The-Reader-Writer-Problem>
## 什么是读者作家问题?
想象一下,你有一个许多线程使用的键值映射数据结构。如果没有写入数据结构,多个线程应该能够同时查找(读取)值。作者不是那么合群 - 为了避免数据损坏,一次只有一个线程可以修改(`write`)数据结构(当时没有读者可能正在阅读)。
这是 _ 读写器问题 _ 的一个例子。也就是说,我们如何有效地同步多个读者和作者,以便多个读者可以一起阅读,但作家获得独家访问?
下面显示了不正确的尝试(“lock”是`pthread_mutex_lock`的简写):
## 尝试#1
|
```
read() {
lock(&m)
// do read stuff
unlock(&m)
}
```
|
```
write() {
lock(&m)
// do write stuff
unlock(&m)
}
```
|
至少我们的第一次尝试不会遭受数据损坏(读者必须在作家写作时等待,反之亦然)!但读者也必须等待其他读者。那么让我们尝试另一种实现..
## 尝试#2:
|
```
read() {
while(writing) {/*spin*/}
reading = 1
// do read stuff
reading = 0
}
```
|
```
write() {
while(reading || writing) {/*spin*/}
writing = 1
// do write stuff
writing = 0
}
```
|
我们的第二次尝试遭遇竞争条件 - 想象两个线程是否同时调用`read`和`write`(或两者都称为写入)。两个线程都可以继续!其次,我们可以有多个读者和多个作者,所以让我们跟踪读者或作者的总数。这让我们尝试#3,
## 尝试#3
请记住`pthread_cond_wait`执行 _ 三个 _ 动作。首先,它以原子方式解锁互斥锁然后休眠(直到它被`pthread_cond_signal`或`pthread_cond_broadcast`唤醒)。第三,唤醒线程必须在返回之前重新获取互斥锁。因此,实际上只有一个线程可以在 lock 和 unlock()方法定义的临界区内运行。
下面的实现#3 确保如果有任何作者写作,读者将进入 cond_wait。
```c
read() {
lock(&m)
while (writing)
cond_wait(&cv, &m)
reading++;
/* Read here! */
reading--
cond_signal(&cv)
unlock(&m)
}
```
然而,只有一个读者一次可以阅读,因为候选人#3 没有解锁互斥锁。更好的版本在阅读之前解锁:
```c
read() {
lock(&m);
while (writing)
cond_wait(&cv, &m)
reading++;
unlock(&m)
/* Read here! */
lock(&m)
reading--
cond_signal(&cv)
unlock(&m)
}
```
这是否意味着作者和阅读可以同时读写?没有!首先,请记住 cond_wait 要求线程在返回之前重新获取互斥锁。因此,一次只有一个线程可以在临界区内执行代码(用**标记)!
```c
read() {
lock(&m);
** while (writing)
** cond_wait(&cv, &m)
** reading++;
unlock(&m)
/* Read here! */
lock(&m)
** reading--
** cond_signal(&cv)
unlock(&m)
}
```
作家必须等待每个人。锁定可确保相互排斥。
```c
write() {
lock(&m);
** while (reading || writing)
** cond_wait(&cv, &m);
** writing++;
**
** /* Write here! */
** writing--;
** cond_signal(&cv);
unlock(&m);
}
```
上面的候选人#3 也使用`pthread_cond_signal`;这只会唤醒一个线程。例如,如果许多读者等待作者完成,那么只有一个睡觉的读者会从他们的睡眠中醒来。读写器应该使用`cond_broadcast`,以便唤醒所有线程并检查它们的 while 循环条件。
## 饥饿的作家
上面的候选人#3 患有饥饿。如果读者经常到达,那么作家永远无法继续(“阅读”计数永远不会减少到零)。这被称为 _ 饥饿 _,并将在重负荷下被发现。我们的解决方法是为作者实现有限等待。如果作家到了,他们仍然需要等待现有的读者,但是未来的读者必须被置于“握笔”中并等待作者完成。 “握笔”可以使用变量和条件变量来实现(这样我们就可以在编写完成后唤醒线程)。
我们的计划是,当作家到来时,在等待当前读者完成之前,记录我们的写作意图(通过递增计数器“作者”)。草绘如下 -
```c
write() {
lock()
writer++
while (reading || writing)
cond_wait
unlock()
...
}
```
当作家非零时,传入的读者将不被允许继续。注意'作家'表示作家已到达,而'阅读'和'写'计数器表示有 _ 有效 _ 读者或作者。
```c
read() {
lock()
// readers that arrive *after* the writer arrived will have to wait here!
while(writer)
cond_wait(&cv,&m)
// readers that arrive while there is an active writer
// will also wait.
while (writing)
cond_wait(&cv,&m)
reading++
unlock
...
}
```
## 尝试#4
以下是我们对 Reader-Writer 问题的第一个解决方案。请注意,如果您继续阅读“Reader Writer 问题”,那么您会发现我们通过给予编写者优先访问锁来解决“第二个 Reader Writer 问题”。该解决方案不是最佳的。然而,它满足了我们原来的问题(N 个活跃的读者,单个活跃的作家,如果有一个持续的读者流,避免作家的饥饿)。
你能找出任何改进吗?例如,你如何改进代码,以便我们只唤醒读者或一个作家?
```c
int writers; // Number writer threads that want to enter the critical section (some or all of these may be blocked)
int writing; // Number of threads that are actually writing inside the C.S. (can only be zero or one)
int reading; // Number of threads that are actually reading inside the C.S.
// if writing !=0 then reading must be zero (and vice versa)
reader() {
lock(&m)
while (writers)
cond_wait(&turn, &m)
// No need to wait while(writing here) because we can only exit the above loop
// when writing is zero
reading++
unlock(&m)
// perform reading here
lock(&m)
reading--
cond_broadcast(&turn)
unlock(&m)
}
writer() {
lock(&m)
writers++
while (reading || writing)
cond_wait(&turn, &m)
writing++
unlock(&m)
// perform writing here
lock(&m)
writing--
writers--
cond_broadcast(&turn)
unlock(&m)
}
```
- UIUC CS241 系统编程中文讲义
- 0. 简介
- #Informal 词汇表
- #Piazza:何时以及如何寻求帮助
- 编程技巧,第 1 部分
- 系统编程短篇小说和歌曲
- 1.学习 C
- C 编程,第 1 部分:简介
- C 编程,第 2 部分:文本输入和输出
- C 编程,第 3 部分:常见问题
- C 编程,第 4 部分:字符串和结构
- C 编程,第 5 部分:调试
- C 编程,复习题
- 2.进程
- 进程,第 1 部分:简介
- 分叉,第 1 部分:简介
- 分叉,第 2 部分:Fork,Exec,等等
- 进程控制,第 1 部分:使用信号等待宏
- 进程复习题
- 3.内存和分配器
- 内存,第 1 部分:堆内存简介
- 内存,第 2 部分:实现内存分配器
- 内存,第 3 部分:粉碎堆栈示例
- 内存复习题
- 4.介绍 Pthreads
- Pthreads,第 1 部分:简介
- Pthreads,第 2 部分:实践中的用法
- Pthreads,第 3 部分:并行问题(奖金)
- Pthread 复习题
- 5.同步
- 同步,第 1 部分:互斥锁
- 同步,第 2 部分:计算信号量
- 同步,第 3 部分:使用互斥锁和信号量
- 同步,第 4 部分:临界区问题
- 同步,第 5 部分:条件变量
- 同步,第 6 部分:实现障碍
- 同步,第 7 部分:读者编写器问题
- 同步,第 8 部分:环形缓冲区示例
- 同步复习题
- 6.死锁
- 死锁,第 1 部分:资源分配图
- 死锁,第 2 部分:死锁条件
- 死锁,第 3 部分:餐饮哲学家
- 死锁复习题
- 7.进程间通信&amp;调度
- 虚拟内存,第 1 部分:虚拟内存简介
- 管道,第 1 部分:管道介绍
- 管道,第 2 部分:管道编程秘密
- 文件,第 1 部分:使用文件
- 调度,第 1 部分:调度过程
- 调度,第 2 部分:调度过程:算法
- IPC 复习题
- 8.网络
- POSIX,第 1 部分:错误处理
- 网络,第 1 部分:简介
- 网络,第 2 部分:使用 getaddrinfo
- 网络,第 3 部分:构建一个简单的 TCP 客户端
- 网络,第 4 部分:构建一个简单的 TCP 服务器
- 网络,第 5 部分:关闭端口,重用端口和其他技巧
- 网络,第 6 部分:创建 UDP 服务器
- 网络,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:远程过程调用简介
- 网络复习题
- 9.文件系统
- 文件系统,第 1 部分:简介
- 文件系统,第 2 部分:文件是 inode(其他一切只是数据...)
- 文件系统,第 3 部分:权限
- 文件系统,第 4 部分:使用目录
- 文件系统,第 5 部分:虚拟文件系统
- 文件系统,第 6 部分:内存映射文件和共享内存
- 文件系统,第 7 部分:可扩展且可靠的文件系统
- 文件系统,第 8 部分:从 Android 设备中删除预装的恶意软件
- 文件系统,第 9 部分:磁盘块示例
- 文件系统复习题
- 10.信号
- 过程控制,第 1 部分:使用信号等待宏
- 信号,第 2 部分:待处理的信号和信号掩码
- 信号,第 3 部分:提高信号
- 信号,第 4 部分:信号
- 信号复习题
- 考试练习题
- 考试主题
- C 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话