虚假唤醒是否会解除所有等待线程的阻塞,甚至是不相关的线程?

Does a spurious wake up unblock all waiting threads, even the unrelated ones?

我对 C++ 中的多线程仍然是个新手,目前我正在努力思考 "spurious wake-ups" 以及导致它们的原因。我已经对条件变量、内核信号、futex 等进行了一些挖掘,并发现了 "spurious wake-ups" 发生的原因和方式的几个罪魁祸首,但仍然有一些我无法找到答案的东西......

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

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

如果这个例子有误导性,我深表歉意……我不知道还能怎么解释。

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

请注意,虚假唤醒不依赖于您阻塞的同步原语或阻塞在该原语上的线程数。它也可能发生在非同步阻塞系统调用中,如 readwrite。通常,您必须假设任何阻塞系统调用都可能出于某种原因过早return,除非像 POSIX 这样的规范保证不会这样做(即使那样,也可能存在错误和OS 个偏离规范的细节)。

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

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

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

除了一次之外,此循环的每次迭代都将被视为虚假。为什么会出现:

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

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

举一个更严肃的例子来说明:

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)
}

这是一个多reader、多写者队列。编写器等到队列中有 space 时,将项目放入,如果队列之前为空,则广播以指示现在有数据。 读者等到队列中有东西时,从队列中取出物品,如果队列以前已满,则广播以表明现在有 space.

请注意条件变量用于两个条件{未满,不为空}。这些是边沿触发的条件:只有从满到空的过渡才会发出信号。

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