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

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

本文关键字:线程 不相关 等待 是否 唤醒 解锁      更新时间:2023-10-16

我仍然不熟悉C++中的多线程,我目前正在尝试围绕"虚假唤醒"以及导致它们的原因。我对条件变量、内核信号、futex 等进行了一些挖掘,发现了几个关于"虚假唤醒"发生的原因和方式的罪魁祸首,但仍然有一些我找不到答案......

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

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

如果示例具有误导性,请道歉...我不知道还能怎么解释。

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

请注意,虚假唤醒不依赖于要阻止的同步基元或该基元上阻止的线程数。非同步阻止系统调用(如readwrite(也可能发生这种情况。通常,您必须假设任何阻塞系统调用都可能出于任何原因过早返回,除非像 POSIX 这样的规范保证不会返回(即使这样,也可能存在偏离规范的错误和操作系统细节(。

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

在条件变量的上下文中,虚假唤醒仅从服务员的角度来看。 这意味着等待已退出,但条件不为真;因此,惯用法是:

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

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

  1. 许多条件被多路复用到单个条件变量上;有时这是合适的,有时它只是懒惰的
  2. 等待线程会
  3. 击败您的线程,并在您有机会拥有它之前更改其状态。
  4. 不相关的事件, 如 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)
}

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

请注意,条件变量用于两个条件{未满,不为空}。 这些是边沿触发的条件:仅发出从满和空的转换信号。

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