虚假唤醒是否会解除所有等待线程的阻塞,甚至是不相关的线程?
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
)。由于信号处理不涉及其他线程,因此它们不会被唤醒。
请注意,虚假唤醒不依赖于您阻塞的同步原语或阻塞在该原语上的线程数。它也可能发生在非同步阻塞系统调用中,如 read
或 write
。通常,您必须假设任何阻塞系统调用都可能出于某种原因过早return,除非像 POSIX 这样的规范保证不会这样做(即使那样,也可能存在错误和OS 个偏离规范的细节)。
有些人将多余的通知归因于虚假唤醒,因为处理这两者通常是相同的。但是,它们并不相同。与虚假唤醒不同,多余的通知实际上是由另一个线程引起的,并且是对条件变量或 futex 进行通知操作的结果。这只是您在未阻塞的线程设法检查唤醒之前检查唤醒可能变为 false 的条件。
条件变量上下文中的虚假唤醒仅来自服务员的角度。表示等待退出,但条件不成立;因此惯用用法是:
Thing.lock()
while Thing.state != Play {
Thing.wait()
}
....
Thing.unlock()
除了一次之外,此循环的每次迭代都将被视为虚假。为什么会出现:
- 许多条件被复用到一个条件变量上;有时这是合适的,有时只是懒惰
- 一个正在等待的线程先于您的线程达到条件,并在您有机会拥有它之前改变了它的状态。
- 不相关的事件,例如 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)
}
这是一个多reader、多写者队列。编写器等到队列中有 space 时,将项目放入,如果队列之前为空,则广播以指示现在有数据。
读者等到队列中有东西时,从队列中取出物品,如果队列以前已满,则广播以表明现在有 space.
请注意条件变量用于两个条件{未满,不为空}。这些是边沿触发的条件:只有从满到空的过渡才会发出信号。
Q_get 和 q_put 保护自己免受由 [1] 和 [2] 引起的虚假唤醒,您可以轻松地检测代码以显示这种情况发生的频率。
我对 C++ 中的多线程仍然是个新手,目前我正在努力思考 "spurious wake-ups" 以及导致它们的原因。我已经对条件变量、内核信号、futex 等进行了一些挖掘,并发现了 "spurious wake-ups" 发生的原因和方式的几个罪魁祸首,但仍然有一些我无法找到答案的东西......
问题: 虚假唤醒是否会解除所有 waiting/blocked 线程的阻塞,甚至是那些等待完全不相关通知的线程?或者是否有单独的阻塞线程等待队列,因此等待另一个通知的线程受到保护?
示例: 假设我们有 249 名斯巴达人在等待进攻波斯人。他们 wait()
他们的领袖列奥尼达 (Leonidas) (第 250 团) notify_all()
何时进攻。现在,在营地的另一边有 49 名受伤的斯巴达战士正在等待医生(第 50 名)notify_one()
以便他可以治疗每个人。一个虚假的唤醒会解锁 所有 等待的斯巴达人,包括受伤的人,还是只会影响等待战斗的人?等待线程是否有两个单独的队列,还是只有一个队列?
如果这个例子有误导性,我深表歉意……我不知道还能怎么解释。
虚假唤醒的原因因操作系统而异,此类唤醒的属性也是如此。例如,在 Linux 中,当信号传递到阻塞线程时会发生唤醒。执行信号处理程序后,线程不会再次阻塞,而是从它被阻塞的系统调用中接收到一个特殊的错误代码(通常是 EINTR
)。由于信号处理不涉及其他线程,因此它们不会被唤醒。
请注意,虚假唤醒不依赖于您阻塞的同步原语或阻塞在该原语上的线程数。它也可能发生在非同步阻塞系统调用中,如 read
或 write
。通常,您必须假设任何阻塞系统调用都可能出于某种原因过早return,除非像 POSIX 这样的规范保证不会这样做(即使那样,也可能存在错误和OS 个偏离规范的细节)。
有些人将多余的通知归因于虚假唤醒,因为处理这两者通常是相同的。但是,它们并不相同。与虚假唤醒不同,多余的通知实际上是由另一个线程引起的,并且是对条件变量或 futex 进行通知操作的结果。这只是您在未阻塞的线程设法检查唤醒之前检查唤醒可能变为 false 的条件。
条件变量上下文中的虚假唤醒仅来自服务员的角度。表示等待退出,但条件不成立;因此惯用用法是:
Thing.lock()
while Thing.state != Play {
Thing.wait()
}
....
Thing.unlock()
除了一次之外,此循环的每次迭代都将被视为虚假。为什么会出现:
- 许多条件被复用到一个条件变量上;有时这是合适的,有时只是懒惰
- 一个正在等待的线程先于您的线程达到条件,并在您有机会拥有它之前改变了它的状态。
- 不相关的事件,例如 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)
}
这是一个多reader、多写者队列。编写器等到队列中有 space 时,将项目放入,如果队列之前为空,则广播以指示现在有数据。 读者等到队列中有东西时,从队列中取出物品,如果队列以前已满,则广播以表明现在有 space.
请注意条件变量用于两个条件{未满,不为空}。这些是边沿触发的条件:只有从满到空的过渡才会发出信号。
Q_get 和 q_put 保护自己免受由 [1] 和 [2] 引起的虚假唤醒,您可以轻松地检测代码以显示这种情况发生的频率。