# 同步,第 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 < 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)
## 为什么存在虚假的尾流?
为了表现。在多 CPU 系统上,竞争条件可能导致唤醒(信号)请求被忽视。内核可能无法检测到此丢失的唤醒呼叫,但可以检测到它何时可能发生。为了避免潜在的丢失信号,线程被唤醒,以便程序代码可以再次测试条件。
## 例
条件变量 _ 总是 _ 与互斥锁一起使用。
在调用 _ 等待 _ 之前,必须锁定互斥锁并且 _ 等待 _ 必须用循环包裹。
```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`的高级使用允许跨进程共享信号量。我们的实现仅适用于同一进程内的线程。
- 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 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话