ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 同步,第 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`的高级使用允许跨进程共享信号量。我们的实现仅适用于同一进程内的线程。