# 同步，第 5 部分：条件变量

> 原文：<https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-5%3A-Condition-Variables>

## 条件变量简介

## 暖身

命名这些属性！

*   “CS 中一次只能有一个进程（/ thread）”
*   “如果等待，那么另一个进程只能进入有限次数的 CS”
*   “如果 CS 中没有其他进程，那么进程可以立即进入 CS”

有关答案，请参见[同步，第 4 部分：临界区问题](/angrave/SystemProgramming/wiki/Synchronization%2C-Part-4%3A-The-Critical-Section-Problem)。

## 什么是条件变量？你怎么用它们？什么是虚假唤醒？

*   条件变量允许一组线程睡眠直到发痒！你可以勾选一个线程或所有正在休眠的线程。如果您只唤醒一个线程，那么操作系统将决定唤醒哪个线程。你不直接唤醒线程，而是“发出”条件变量，然后唤醒条件变量内部的一个（或所有）线程。

*   条件变量与互斥锁和循环一起使用（以检查条件）。

*   偶尔等待的线程可能会无缘无故地唤醒（这被称为 _ 虚假唤醒 _）！这不是问题，因为您总是在循环中使用`wait`来测试必须为 true 才能继续的条件。

*   通过调用`pthread_cond_broadcast`（全部唤醒）或`pthread_cond_signal`（唤醒一个）唤醒在条件变量内睡眠的线程。注意尽管有函数名称，这与 POSIX `signal`无关！

## `pthread_cond_wait`有什么作用？

调用`pthread_cond_wait`执行三个操作：

*   解锁互斥锁
*   等待（在相同的条件变量上调用`pthread_cond_signal`时休眠）
*   在返回之前，锁定互斥锁

## （高级主题）为什么条件变量也需要互斥锁？

条件变量需要互斥锁有三个原因。最简单的理解是它可以防止早期唤醒消息（`signal`或`broadcast`功能）被“丢失”。想象一下，在调用 _ `pthread_cond_wait`之前，满足条件的下列事件序列（时间向下运行）。在这个例子中，唤醒信号丢失了！

| 线程 1 | 线程 2 |
| --- | --- |
| `while( answer &lt; 42) {` |  |
|  | `answer++` |
|  | `p_cond_signal(cv)` |
| `p_cond_wait(cv,m)` |  |

如果两个线程都锁定了互斥锁，则在 `pthread_cond_wait(cv, m)`被调用（然后在内部解锁互斥锁之后）_ 之前无法发送信号 _

第二个常见原因是更新程序状态（`answer`变量）通常需要互斥 - 例如，多个线程可能正在更新`answer`的值。

第三个也是微妙的原因是为了满足我们在此仅概述的实时调度问题：在时间关键型应用中，应该允许具有 _ 最高优先级 _ 的等待线程首先继续。为满足此要求，还必须在调用`pthread_cond_signal`或`pthread_cond_broadcast`之前锁定互斥锁。对于好奇的人来说，[在](https://groups.google.com/forum/?hl=ky#!msg/comp.programming.threads/wEUgPq541v8/ZByyyS8acqMJ)[中进行了较长时间的历史性讨论。](https://groups.google.com/forum/?hl=ky#!msg/comp.programming.threads/wEUgPq541v8/ZByyyS8acqMJ)

## 为什么存在虚假的尾流？

为了表现。在多 CP​​U 系统上，竞争条件可能导致唤醒（信号）请求被忽视。内核可能无法检测到此丢失的唤醒呼叫，但可以检测到它何时可能发生。为了避免潜在的丢失信号，线程被唤醒，以便程序代码可以再次测试条件。

## 例

条件变量 _ 总是 _ 与互斥锁一起使用。

在调用 _ 等待 _ 之前，必须锁定互斥锁并且 _ 等待 _ 必须用循环包裹。

```c
pthread_cond_t cv;
pthread_mutex_t m;
int count;

// Initialize
pthread_cond_init(&cv, NULL);
pthread_mutex_init(&m, NULL);
count = 0;

pthread_mutex_lock(&m);
while (count < 10) {
   pthread_cond_wait(&cv, &m); 
/* Remember that cond_wait unlocks the mutex before blocking (waiting)! */
/* After unlocking, other threads can claim the mutex. */
/* When this thread is later woken it will */
/* re-lock the mutex before returning */
}
pthread_mutex_unlock(&m);

//later clean up with pthread_cond_destroy(&cv); and mutex_destroy 

// In another thread increment count:
while (1) {
  pthread_mutex_lock(&m);
  count++;
  pthread_cond_signal(&cv);
  /* Even though the other thread is woken up it cannot not return */
  /* from pthread_cond_wait until we have unlocked the mutex. This is */
  /* a good thing! In fact, it is usually the best practice to call */
  /* cond_signal or cond_broadcast before unlocking the mutex */
  pthread_mutex_unlock(&m);
}
```

## 实现计数信号量

*   我们可以使用条件变量实现计数信号量。
*   每个信号量都需要一个计数，一个条件变量和一个互斥量

```c
typedef struct sem_t {
  int count; 
  pthread_mutex_t m;
  pthread_condition_t cv;
} sem_t;
```

实现`sem_init`以初始化互斥锁和条件变量

```c
int sem_init(sem_t *s, int pshared, int value) {
  if (pshared) { errno = ENOSYS /* 'Not implemented'*/; return -1;}

  s->count = value;
  pthread_mutex_init(&s->m, NULL);
  pthread_cond_init(&s->cv, NULL);
  return 0;
}
```

我们`sem_post`的实现需要增加计数。我们还将唤醒在条件变量内部休眠的任何线程。请注意，我们锁定和解锁互斥锁，因此一次只有一个线程可以在临界区内。

```c
sem_post(sem_t *s) {
  pthread_mutex_lock(&s->m);
  s->count++;
  pthread_cond_signal(&s->cv); /* See note */
  /* A woken thread must acquire the lock, so it will also have to wait until we call unlock*/

  pthread_mutex_unlock(&s->m);
}
```

如果信号量的计数为零，我们的`sem_wait`实现可能需要休眠。就像`sem_post`一样，我们使用锁来包装临界区（因此一次只有一个线程可以执行我们的代码）。请注意，如果线程确实需要等待，那么互斥锁将被解锁，允许另一个线程进入`sem_post`并从我们的睡眠中唤醒我们！

请注意，即使线程被唤醒，在它从`pthread_cond_wait`返回之前，它必须重新获取锁，因此它必须再等一点（例如，直到 sem_post 结束）。

```c
sem_wait(sem_t *s) {
  pthread_mutex_lock(&s->m);
  while (s->count == 0) {
      pthread_cond_wait(&s->cv, &s->m); /*unlock mutex, wait, relock mutex*/
  }
  s->count--;
  pthread_mutex_unlock(&s->m);
}
```

**等`sem_post`一直调用`pthread_cond_signal`不会破坏 sem_wait？** 答案：不！在计数非零之前，我们无法通过循环。在实践中，这意味着即使没有等待线程，`sem_post`也会不必要地调用`pthread_cond_signal`。更有效的实施只会在必要时调用`pthread_cond_signal`，即

```c
  /* Did we increment from zero to one- time to signal a thread sleeping inside sem_post */
  if (s->count == 1) /* Wake up one waiting thread!*/
     pthread_cond_signal(&s->cv);
```

## 其他信号量考虑因素

*   真实的信号量实现包括队列和调度问题，以确保公平性和优先级，例如唤醒最高优先级的最长睡眠线程。
*   此外，`sem_init`的高级使用允许跨进程共享信号量。我们的实现仅适用于同一进程内的线程。