1

我对 C++ 中的多线程仍然很陌生,我目前正试图围绕“虚假唤醒”以及导致它们的原因。我已经对条件变量、内核信号、futex 等进行了一些挖掘,并发现了一些关于“虚假唤醒”为何以及如何发生的罪魁祸首,但仍有一些我无法找到答案。 .

问题:虚假唤醒是否会解除所有等待/阻塞线程的阻塞,甚至是等待完全不相关通知的线程?或者阻塞线程是否有单独的等待队列,因此等待另一个通知的线程受到保护?

示例:假设我们有 249 名斯巴达人等待攻击波斯人。他们wait()为他们的领袖列奥尼达斯(第 250 人)要notify_all()何时进攻。现在,在营地的另一边,有 49 名受伤的斯巴达人正在等待医生(第 50 名)来notify_one()治疗每个人。虚假的唤醒会解除所有等待的斯巴达人,包括受伤的斯巴达人,还是只会影响等待战斗的人?等待线程有两个单独的队列,还是只有一个?

如果该示例具有误导性,我深表歉意……我不知道如何解释它。

4

2 回答 2

2

在条件变量的上下文中,虚假唤醒只是从服务员的角度来看。表示等待退出,但条件不成立;因此惯用用法是:

Thing.lock()
 while Thing.state != Play {
     Thing.wait()
 }
 ....
 Thing.unlock()

该循环的每次迭代,但一次,将被认为是虚假的。为什么会出现:

  1. 许多条件被复用到单个条件变量上;有时这是合适的,有时只是懒惰
  2. 等待线程将您的线程击败到条件,并在您有机会拥有它之前更改了它的状态。
  3. 不相关的事件,例如 kill(2) 处理这样做是为了确保异步处理程序运行后的一致性。

最重要的是验证是否满足期望的条件,如果不满足则重试或放弃。重新检查很难诊断的情况是一个常见的错误。

作为一个更严肃的例子应该说明:

int q_next(Q *q, int idx) {
/* return the q index succeeding this, with wrap */
   if (idx + 1 == q->len) {
       return 0
   } else {
       return idx + 1
   }
}
void q_get(Q *q, Item *p) {
    Lock(q)
    while (q->head == q->tail) {
         Wait(q)
    }
    *p = q->data[q->tail]

    if (q_next(q, q->head) == q->tail) {
        /* q was full, now has space */
        Broadcast(q)
    }
    q->tail = q_next(q, q->tail)
    Unlock(q)
}
void q_put(Q *q, Item *p) {
    Lock(q)
    while (q_next(q, q->head) == q->tail) {
         Wait(q)
    }
    q->data[q->head] = *p
    if (q->head == q->tail) {
        /* q was empty, data available */
        Broadcast(q)
    }
    q->head = q_next(q, q->head)
    Unlock(q)
}

这是一个多读者、多作者的队列。写入者等到队列中有空间时,将项目放入,如果队列之前为空,则广播以指示现在有数据。读者等到队列中有东西,从队列中取出项目,如果队列之前已满,则广播以指示现在有空间。

请注意,条件变量用于两个条件 {not full, not empty}。这些是边沿触发的条件:只有从满和从空的转换才会发出信号。

Q_get 和 q_put 保护自己免受由 [1] 和 [2] 引起的虚假唤醒,您可以轻松地检测代码以显示这种情况发生的频率。

于 2020-02-09T15:55:17.827 回答
2

虚假唤醒的原因因每个操作系统而异,此类唤醒的属性也是如此。例如,在 Linux 中,当信号传递到阻塞线程时会发生唤醒。执行信号处理程序后,线程不会再次阻塞,而是从EINTR被阻塞的系统调用中接收到一个特殊的错误代码(通常是 )。由于信号处理不涉及其他线程,因此它们不会被唤醒。

请注意,虚假唤醒不取决于您阻塞的同步原语或该原语上阻塞的线程数。read它也可能发生在诸如or之类的非同步阻塞系统调用write中。通常,您必须假设任何阻塞系统调用都可能因任何原因过早返回,除非像 POSIX 之类的规范保证不会(即使这样,也可能存在偏离规范的错误和操作系统细节)。

有些人将多余的通知归因于虚假唤醒,因为处理两者通常是相同的。但是,它们并不相同。与虚假唤醒不同,多余的通知实际上是由另一个线程引起的,并且是对条件变量或 futex 进行通知操作的结果。这只是您在唤醒时检查的条件可能会在未阻塞线程设法检查它之前变为 false。

于 2020-02-09T11:40:34.280 回答