是否保证pthread_cond_signal会唤醒等待的线程?

时间:2013-03-18 20:31:54

标签: c pthreads condition-variable

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

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

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

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

3 个答案:

答案 0 :(得分:13)

快速回答:

pthread_cond_signal()将唤醒至少一个在条件变量上被阻止的线程 - 但不能保证更多(请参考,使用pthread_cond_broadcast()唤醒所有被阻止的线程。)

来自here

  

pthread_cond_signal()调用取消阻塞至少一个线程   在指定的条件变量cond上被阻塞(如果有的话)   线程在cond上被阻止)。

     

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

上被阻止

答案越长:

因此,根据规范,我假设unblocking同步发生,也就是说,第一次调用pthread_cond_signal()时已被解除阻塞的线程将被第二次调用{{ {1}},因此其他线程将被唤醒。

但是,我不知道你的具体pthread实现是否属于这种情况(目前glibc网站非常狡猾,因此无法访问代码进行查看)。

可能尚未实施,但在规范中的答案:

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

可以找到关于这个主题的长时间讨论here,新规范是:

  

pthread_cond_broadcast()和pthread_cond_signal()函数   应自动确定哪些线程(如果有)被阻止   在指定的条件变量cond上。这个决心   应在此期间的非特定时间发生   pthread_cond_broadcast()或pthread_cond_signal()调用。   然后pthread_cond_broadcast()函数将解除所有阻塞   这些线程。 pthread_cond_signal()函数将取消阻塞   至少有一个这样的线程。

所以,结论是: 如果不是规范的专家解释器,我会说新文本支持同步发生这种情况的假设 - 这样两个连续调用pthread_cond_broadcast()并且有两个被阻塞的线程可用,将唤醒两个线程。

我对此并不是100%肯定,所以如果有人可以详细说明,请随时这样做。

答案 1 :(得分:1)

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

<块引用>

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

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

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

<块引用>

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

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

如果服务员只是旋转,这将是直接实现的,但我们需要让他们使用 futex 阻止。 Futex 不保证按 FIFO 顺序唤醒,因此如果我们只使用单个 futex,我们无法可靠地唤醒符合条件的服务员。还有,futex word的大小是32b,但是我们需要区分超过1<<32个状态,因为我们需要表示唤醒的顺序(以及哪些waiter有资格消费信号); futex 中的阻塞不是由服务员确定其在服务员序列中的位置的原子操作,因此我们需要 futex 字来可靠地通知服务员他们不应再尝试阻塞,因为在此期间他们已经收到了信号。虽然 32b 值的 ABA 问题很少见,但在我们意识到它时忽略它也不是正确的做法。

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

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

由于进程共享 condvars,我们无法分配新内存,因此我们只有两个组插槽,可以在 G1 和 G2 之间更改其角色。每个都有一个单独的 futex 字、一些可供消费的信号、一个大小(组中尚未发出信号的服务员的数量)和一个引用计数。

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

为了表示组覆盖的服务员序列中的哪些间隔(以及哪个组槽包含 G1 或 G2),我们使用 64b 计数器来指定 G1(包括)的开始位置,以及服务员中的单个位序列计数器来表示当前包含 G2 的组槽。这允许我们以原子方式切换组角色。服务员在服务员序列中获得一个位置。 G1 开始位置允许服务员确定他们是否在一个已经完全发出信号的组中(即,当前的 G1 是否在服务员位置之后的位置开始)。服务员无法确定他们当前是在 G2 还是 G1——但他们也没有,因为他们感兴趣的是是否有可用的信号,而且他们总是从 G2 开始(他们知道 G2 的组位置,因为在服务员序列。信号员将简单地填充正确的组,直到它完全发出信号并且可以关闭(他们不会切换组角色,直到他们真的必须减少不得不等待服务员仍然持有现在关闭的参考的可能性G1).

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

为了实现 condvar 销毁要求(即 pthread_cond_destroy 可以在所有服务员都收到信号后立即调用),服务员在开始等待之前增加一个引用计数,并在他们停止等待后减少它但就在他们获得与 condvar 关联的互斥锁之前。

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

__wseq:服务员序列计数器

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

PTHREAD_COND_INITIALIZER condvar 的所有字段都设置为零,从而产生一个 condvar,其中 G2 从位置 0 开始,G1 关闭。

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

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

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

限制:

  • condvar 的设计不允许超过 __PTHREAD_COND_MAX_GROUP_SIZE * (1 << 31) 次调用 __pthread_cond_wait
  • 不支持超过 __PTHREAD_COND_MAX_GROUP_SIZE 个并发等待。
  • 除了 POSIX 允许的错误或记录的错误之外,我们还可以返回以下错误:
    • EPERM 如果 MUTEX 是递归互斥体并且调用者不拥有它。
    • EOWNERDEADENOTRECOVERABLE 使用强大的互斥锁时。与其他错误不同,当我们重新获取互斥锁时可能会发生这种情况;这是 POSIX 不允许的(它要求在我们释放互斥锁或更改 condvar 状态之前几乎所有错误都发生),但我们实际上无能为力。
    • 使用 PTHREAD_MUTEX_PP_* mutexes 时,我们还可以返回 __pthread_tpp_change_priority 返回的所有错误。在这种情况下,我们已经释放了互斥锁,因此调用者不能期望拥有 MUTEX。

其他注意事项:

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

从该文档中我们了解到您可以从任何地方调用 pthread_cond_signal()pthread_cond_broadcast()。如果你从锁的外部调用这些函数,那么除了:

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

但是,如果您使用互斥锁并从锁定区域内调用 pthread_cond_signal(),那么每次调用都会唤醒一个线程。 (但请注意,您的所有 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)

答案 2 :(得分:0)

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

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

一个锁将有一个已经进入休眠状态的线程队列,因为它们试图获取它但是它被另一个线程持有。

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

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

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

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