原文:https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-5%3A-Condition-Variables
命名这些属性!
- “CS 中一次只能有一个进程(/ thread)”
- “如果等待,那么另一个进程只能进入有限次数的 CS”
- “如果 CS 中没有其他进程,那么进程可以立即进入 CS”
有关答案,请参见同步,第 4 部分:临界区问题。
-
条件变量允许一组线程睡眠直到发痒!你可以勾选一个线程或所有正在休眠的线程。如果您只唤醒一个线程,那么操作系统将决定唤醒哪个线程。你不直接唤醒线程,而是“发出”条件变量,然后唤醒条件变量内部的一个(或所有)线程。
-
条件变量与互斥锁和循环一起使用(以检查条件)。
-
偶尔等待的线程可能会无缘无故地唤醒(这被称为 _ 虚假唤醒 _)!这不是问题,因为您总是在循环中使用
wait
来测试必须为 true 才能继续的条件。 -
通过调用
pthread_cond_broadcast
(全部唤醒)或pthread_cond_signal
(唤醒一个)唤醒在条件变量内睡眠的线程。注意尽管有函数名称,这与 POSIXsignal
无关!
调用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
之前锁定互斥锁。对于好奇的人来说,在中进行了较长时间的历史性讨论。
为了表现。在多 CPU 系统上,竞争条件可能导致唤醒(信号)请求被忽视。内核可能无法检测到此丢失的唤醒呼叫,但可以检测到它何时可能发生。为了避免潜在的丢失信号,线程被唤醒,以便程序代码可以再次测试条件。
条件变量 _ 总是 _ 与互斥锁一起使用。
在调用 _ 等待 _ 之前,必须锁定互斥锁并且 _ 等待 _ 必须用循环包裹。
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);
}
- 我们可以使用条件变量实现计数信号量。
- 每个信号量都需要一个计数,一个条件变量和一个互斥量
typedef struct sem_t {
int count;
pthread_mutex_t m;
pthread_condition_t cv;
} sem_t;
实现sem_init
以初始化互斥锁和条件变量
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
的实现需要增加计数。我们还将唤醒在条件变量内部休眠的任何线程。请注意,我们锁定和解锁互斥锁,因此一次只有一个线程可以在临界区内。
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 结束)。
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
,即
/* 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
的高级使用允许跨进程共享信号量。我们的实现仅适用于同一进程内的线程。