Skip to content

Latest commit

 

History

History
162 lines (116 loc) · 6.95 KB

35.md

File metadata and controls

162 lines (116 loc) · 6.95 KB

同步,第 5 部分:条件变量

原文: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(唤醒一个)唤醒在条件变量内睡眠的线程。注意尽管有函数名称,这与 POSIX signal无关!

pthread_cond_wait有什么作用?

调用pthread_cond_wait执行三个操作:

  • 解锁互斥锁
  • 等待(在相同的条件变量上调用pthread_cond_signal时休眠)
  • 在返回之前,锁定互斥锁

(高级主题)为什么条件变量也需要互斥锁?

条件变量需要互斥锁有三个原因。最简单的理解是它可以防止早期唤醒消息(signalbroadcast功能)被“丢失”。想象一下,在调用 _ 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_signalpthread_cond_broadcast之前锁定互斥锁。对于好奇的人来说,中进行了较长时间的历史性讨论。

为什么存在虚假的尾流?

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

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

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

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的高级使用允许跨进程共享信号量。我们的实现仅适用于同一进程内的线程。