# 同步，第 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 &#124;&#124; 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)  
}
```