12

这是一个普遍的问题。例如,当前有两个子线程调用pthread_cond_wait(&cond1,&mutex)了,它们都在等待。然后,父线程调用

pthread_cond_signal(&cond1);
pthread_cond_signal(&cond1);

接下来,我的问题是,是否保证两个等待线程都会被唤醒?(假设第一个被唤醒的线程稍后在某个执行阶段释放互斥锁,以便第二个线程可以获取它)。

我问这个问题的原因是,对于 Unix 系统级别的信号,信号(比如SIGCHLD)没有排队,因此如果连续传递多个相同类型的信号,它们可能会丢失。所以我想知道是pthread_cond_signal用不同的方式实现的,这样如果调度程序碰巧让父线程连续两次发出信号,它们就不会丢失?

4

3 回答 3

13

快速回答:

pthread_cond_signal()将唤醒至少一个在条件变量上被阻塞的线程 - 但不能保证超过这个值(作为参考,用于pthread_cond_broadcast()唤醒所有被阻塞的线程)。

这里

pthread_cond_signal() 调用至少解除阻塞在指定条件变量 cond 上的线程之一(如果有任何线程在 cond 上阻塞)。

pthread_cond_broadcast() 调用解除对指定条件变量 cond 上当前阻塞的所有线程的阻塞。

更长的答案:

所以,根据规范,我假设解除阻塞是同步发生的,也就是说,第一次调用解除阻塞的线程将被pthread_cond_signal()第二次调用视为解除阻塞pthread_cond_signal(),因此另一个线程将被唤醒向上。

但是,我不知道您的特定 pthread 实现是否是这种情况(并且 glibc 网站目前非常狡猾,因此无法访问代码查看)。

可能尚未实现但符合规范的答案:

不过应该注意的是,该规范最近对如何确定在给定条件变量上实际阻塞哪些线程进行了略微改写pthread_cond_signal()pthread_cond_broadcast()但我认为并非所有实现都已经赶上。

可以在此处找到有关该主题的长时间讨论,新规范为:

pthread_cond_broadcast() 和 pthread_cond_signal() 函数应自动确定哪些线程(如果有)在指定的条件变量 cond 上被阻塞。该确定应在 pthread_cond_broadcast() 或 pthread_cond_signal() 调用期间的未指定时间发生。然后 pthread_cond_broadcast() 函数应解除对所有这些线程的阻塞。pthread_cond_signal() 函数应至少解除对这些线程之一的阻塞。

所以,结论:如果不是规范的专家解释者,我会说新文本支持这种同步发生的假设 - 因此两个连续调用pthread_cond_signal()两个可用的阻塞线程将唤醒两个线程。

不过,我不是 100% 确定这一点,所以如果有人可以详细说明,请随时这样做。

于 2013-03-18T20:35:36.293 回答
1

查看pthread_cond_signal()实现,有一条注释简要解释了代码的作用:

加载服务员序列号,它代表我们对任何服务员的相对排序。宽松的 MO 就足够了,因为:

  1. 我们可以选择外部happens-before约束允许的任何位置。特别是,如果__pthread_cond_wait我们之前发生了另一个电话,这个服务员必须有资格被我们叫醒。建立这种发生之前的唯一方法是在获得与 关联的互斥锁时发出信号,condvar并确保信号的关键部分发生在等待者之后。因此,互斥锁确保我们看到服务员的__wseq增加。
  2. 一旦我们选择了一个位置,我们就不需要通过发生在我们设置的之前将其传达给程序:首先,任何唤醒都可能是虚假唤醒,因此程序不能解释唤醒作为服务员在特定信号之前发生的指示;其次,程序无法检测服务员是否尚未被唤醒(即,它无法区分未唤醒的服务员和已被唤醒但尚未恢复执行的服务员),因此它无法尝试推断出信号发生在特定服务员之前。

如前所述,在pthread_cond_wait()实现中有更多关于算法的信息:

这种condvar实现保证所有对信号和广播的调用以及每个等待调用的所有三个虚拟原子部分(即,(1)释放互斥体并阻塞,(2)解除阻塞,以及(3)重新获取互斥体)以某种与调用程序中的发生前关系一致的总顺序发生。但是,此顺序不一定会导致建立额外的先发生关系(这与允许的虚假唤醒非常吻合)。

__wseq所有服务员都在 64b 服务员序列 ( )中获得特定位置。此序列确定允许哪些服务员使用信号。广播等于发送与未阻塞的服务员一样多的信号。__wseq当一个信号到达时,它以一个宽松的MO负载(即下一个服务员将获得的位置)对当前值进行采样。(这已经足够了,因为它与happens-before一致;调用者可以通过在持有互斥锁的同时调用信号来强制执行更强的排序约束。)只有位置小于__wseq信号观察到的值的等待者才有资格使用这个信号。

如果服务员只是旋转,这将很容易实现,但我们需要让他们使用 futexes 阻塞。Futex 不能保证按 FIFO 顺序唤醒,所以如果我们只使用一个 futex,我们就不能可靠地唤醒符合条件的服务员。另外,futex 词的大小是 32b,但是我们需要区分超过 1<<32 个状态,因为我们需要表示唤醒的顺序(以及因此哪些服务员有资格消费信号);futex 中的阻塞不是原子的,等待者确定其在等待者序列中的位置,因此我们需要 futex 字来可靠地通知等待者他们不应再尝试阻塞,因为同时它们已经被发出信号。虽然 32b 值的 ABA 问题很少见,但当我们意识到它时忽略它也不是正确的做法。

因此,我们使用 64b 的计数器来表示等待者序列(在仅支持 32b 原子的架构上,我们少用了几位)。为了使用 futexes 处理阻塞,我们维护了两组等待者:

  • G1 组由所有有资格消费信号的服务员组成;传入信号将始终向该组中的服务员发出信号,直到 G1 中的所有服务员都已发出信号。
  • G2 组由在 G1 出现时到达的服务员组成,并且仍然包含尚未发出信号的服务员。当 G1 中的所有服务员都发出信号并且有新的信号到达时,新的信号会将 G2 转换为新的 G1,并为未来的服务员创建一个新的 G2。

由于 process-shared ,我们无法分配新内存condvars,所以我们只有两个组槽,它们在 G1 和 G2 之间改变角色。每个都有一个单独的 futex 字、许多可供消费的信号、一个大小(组中尚未发出信号的服务员的数量)和一个引用计数。

组引用计数用于维护正在使用组的 futex 的服务员数量。在一个组可以改变它的角色之前,引用计数必须表明没有服务员不再使用 futex;这可以防止 futex 字上的 ABA 问题。

为了表示这些组覆盖了等待者序列中的哪些间隔(以及因此哪个组槽包含 G1 或 G2),我们使用 64b 计数器来指定 G1(包括)的起始位置,并使用等待者序列计数器中的单个位来表示表示当前包含 G2 的组槽。这允许我们以原子方式切换组角色。服务员在服务员序列中获得一个位置。G1 开始位置允许服务员确定他们是否在已经完全发出信号的组中(即,当前 G1 是否在服务员位置之后的位置开始)。服务员无法确定他们目前是在 G2 还是 G1 - 但他们也没有,因为他们只关心是否有可用的信号,

信号员保持 G1 的初始大小,以便能够确定 G2 从何处开始(G2 在变为 G1 之前始终是开放式的)。他们跟踪一个组的剩余大小;当服务员取消等待时(由于 PThreads 取消或超时),他们也会减少这个剩余大小。

为了实现condvar销毁要求(即,pthread_cond_destroy可以在所有等待者都收到信号后立即调用),等待者在开始等待之前增加一个引用计数,并在他们停止等待之后但就在他们获取与condvar.

pthread_cond_t因此由以下内容组成(用于标志的位,不是每个字段的主值的一部分,但对于使某些事物具有原子性或因为数据结构中的其他地方没有空间容纳它们是必需的):

__wseq:服务员序列计数器

  • LSB 是当前 G2 的索引。
  • 服务员在获取与condvar. Signalers 加载它并同时对其进行 fetch-xor。 __g1_start:G1(含)起始位置
  • LSB 是当前 G2 的索引。
  • 在获得condvar-internal 锁并由服务员同时观察时由信号员修改。 __g1_orig_size: G1 的初始大小
  • 两个最低有效位表示condvar-internal 锁。
  • 仅在获得condvar-internal 锁时访问。 __wrefs: 服务员参考计数器。
  • 如果服务员在删除最后一个引用时应该运行 futex_wake,则位 2 为真。 pthread_cond_destroy将此用作 futex 字。
  • 第 1 位是时钟 ID ( 0 == CLOCK_REALTIME, 1 == CLOCK_MONOTONIC)。
  • 如果这是一个进程共享,则位 0 为真condvar
  • 服务员和 pthread_cond_destroy 使用的简单引用计数。(如果格式__wrefs改变,更新nptl_lock_constants.pysym和漂亮的打印机。)对于这两组中的每一个,我们都有: __g_refs: Futex 服务员引用计数。
  • futex_wake如果服务员在删除最后一个引用时应该运行,则 LSB 为真。
  • 服务员与已获取 condvar 内部锁的信号器同时使用的引用计数。 __g_signals:仍然可以消耗的信号数。
  • 被服务员用作 futex 词。服务员和信号员同时使用。
  • 如果该组已完全发出信号(即,它已关闭),则 LSB 为真。 __g_size:保留在该组中的服务员(即尚未发出信号的服务员。
  • 由取消等待的信号器和服务员访问(两者都只有在获得condvar-internal 锁时才会这样做。
  • G2 的大小始终为零,因为直到组变为 G1 才能确定。
  • 尽管这是无符号类型,但我们依靠使用无符号溢出规则来使其有效地保持负值(特别是当 G2 中的服务员取消等待时)。

APTHREAD_COND_INITIALIZER condvar的所有字段都设置为零,这会产生condvarG2 从位置 0 开始的 a 和封闭的 G1。

因为服务员在获取位置时不声明对组的所有权,__wseq但在使用 futex 阻塞时只对组进行引用计数,因此可能会在服务员增加引用计数之前关闭组。因此,服务员必须使用 . 检查他们的组是否已经关闭__g1_start。他们还必须在试图从__g_signals. 请注意,对于这些检查,使用宽松的 MO 加载__g1_start就足够了,因为如果服务员可以看到足够大的值,它也可能会消耗服务员组中的信号。

服务员试图在__g_signals不持有引用计数的情况下从其中获取信号,这可能导致在他们自己的组已经关闭后从更新的组中窃取信号。他们不能总是检测到他们是否真的偷了,因为他们不知道他们什么时候偷了,但他们可以保守地向他们偷窃的群体添加一个信号;如果他们不必要地这样做,所发生的一切都是虚假的唤醒。为了降低这种可能性,__g1_start还包含当前 g2 的索引,这允许服务员检查组槽上是否存在别名;如果没有,他们没有从当前的 G1 窃取,这意味着他们窃取的 G1 肯定已经关闭,他们不需要修复任何东西。

中的最后一个字段必须pthread_cond_t__g_signals[1]: 前一个condvar使用了一个指针大小的字段 in pthread_cond_t,因此PTHREAD_COND_INITIALIZERcondvar实现中的 a 可能仅将 4 个字节初始化为零,而不是我们需要的 8 个字节(即总共 44 个字节而不是 48 个字节)我们需要)。 __g_signals[1]在第一个组切换(G2 从索引 0 开始)之前不被访问,这将在无害提取之后将其值设置为零 - 或者其返回值被忽略。这有效地完成了初始化。

限制:

  • condvar不仅仅是为了允许__PTHREAD_COND_MAX_GROUP_SIZE * (1 << 31)调用__pthread_cond_wait.
  • __PTHREAD_COND_MAX_GROUP_SIZE不支持多于并发的服务员。
  • 除了 POSIX 允许的错误或记录在案的错误之外,我们还可以返回以下错误:
    • EPERM如果 MUTEX 是递归互斥体并且调用者不拥有它。
    • EOWNERDEAD或者ENOTRECOVERABLE在使用健壮的互斥锁时。与其他错误不同,这可能在我们重新获取互斥锁时发生;POSIX 不允许这样做(这要求所有错误在我们释放互斥锁或更改condvar状态之前虚拟发生),但我们真的无能为力。
    • 使用的时候PTHREAD_MUTEX_PP_* mutexes,我们也可以返回所有返回的错误__pthread_tpp_change_priority。在这种情况下,我们已经释放了互斥锁,所以调用者不能期望拥有 MUTEX。

其他注意事项:

  • 我们使用 __pthread_mutex_unlock_usercnt(m, 0) / __pthread_mutex_cond_lock(m) 代替普通的互斥锁解锁/锁定函数,因为这些不会改变互斥锁内部用户计数,因此可以在 condvar 仍与特定关联时检测到互斥锁,因为有一个服务员在使用这个互斥锁的这个 condvar 上被阻塞。

从该文档中我们了解到,您可以从任何地方调用pthread_cond_signal()and pthread_cond_broadcast()。如果您从锁外部调用这些函数,那么除了以下内容之外,您没有非常强的保证:

  1. pthread_cond_signal()至少唤醒一个线程,但如果两个调用同时到达那里,那么同一个线程可能会被拾取两次。
  2. pthread_cond_broadcast()无论如何都会唤醒所有线程。

但是,如果您使用互斥锁并pthread_cond_signal()在锁定区域内调用 from,则每次调用都会唤醒一个线程。(但是请注意,您的所有人都pthread_cond_signal()应该受到保护。)

因此,以下代码将被认为是安全的:

pthread_mutex_lock(mutex);
...
pthread_cond_signal(cond1);    // no mutex reference, these calls could happen
pthread_cond_signal(cond1);    // while the mutex is not locked...
pthread_cond_unlock(mutex);

并且等待也使用锁:

pthread_mutex_lock(mutex);
...
pthread_cond_wait(cond1, mutex);
...
pthread_mutex_unlock(mutex);

由于我们使用的是锁定的互斥体,因此信号和等待在内部按顺序处理,因此它完全按预期工作。

虽然有限制,但我们可能无法在普通应用程序中真正达到。例如__PTHREAD_COND_MAX_GROUP_SIZE,它代表服务员的最大数量,并且是一个疯狂的大数:

#define __PTHREAD_COND_MAX_GROUP_SIZE ((unsigned) 1 << 29)
于 2021-02-26T01:31:21.827 回答
0

我知道这是一个旧线程(没有双关语),但典型的实现是这样的:

条件变量中将有一个当前处于睡眠状态的线程队列,等待它发出信号。

锁将有一个线程队列,这些线程已被置于睡眠状态,因为它们试图获取它但它被另一个线程持有。

cond_wait 将正在运行的线程添加到条件变量的队列中,释放锁,并使自己进入睡眠状态。

cond_signal 只是将一个休眠线程从条件变量队列移动到锁队列。

当正在运行的线程释放锁时,从锁的队列中移除一个休眠线程,锁的所有权转移给该休眠线程,并且该休眠线程被唤醒。

不要问我为什么规范说 cond_signal 可能会唤醒多个线程......

于 2017-06-25T23:01:25.807 回答