3

假设在信号线程修改影响谓词真值的状态并调用pthread_cond_signal而不持有与条件变量关联的互斥锁的情况下使用条件变量?这种类型的使用是否总是受制于可能错过信号的竞争条件?

对我来说,似乎总是有一个明显的种族:

  1. 服务员将谓词评估为假,但在它可以开始等待之前......
  2. 另一个线程以使谓词为真的方式更改状态。
  3. 另一个线程调用pthread_cond_signal,它什么也不做,因为还没有服务员。
  4. 服务员线程进入pthread_cond_wait,不知道谓词现在为真,并无限期地等待。

但是,如果情况发生变化,是否总是存在相同类型的竞争条件,以便(A)在调用时保持互斥锁,而pthread_cond_signal不是在更改状态时保持,或者(B)在更改状态时保持互斥锁,只是不是在打电话的时候pthread_cond_signal

我是从想知道上述非最佳实践用法是否有任何有效用途的角度来问的,即正确的条件变量实现是否需要在避免竞争条件本身时考虑这种用法,或者它是否可以忽略它们,因为它们本来就很活泼。

4

3 回答 3

2

必须在互斥体中修改状态,如果没有其他原因,可能会导致虚假唤醒,这将导致读取器读取状态,而写入器正在写入状态。

您可以pthread_cond_signal在状态更改后随时调用。它不必在互斥锁内。POSIX 保证至少有一个服务员会醒来检查新的状态。更重要的是:

  1. 调用pthread_cond_signal并不能保证读取器将首先获取互斥锁。另一位作家可能会在读者有机会检查新状态之前进入。条件变量并不能保证读者立即跟随作者(毕竟,如果没有读者怎么办?)
  2. 在释放锁之后调用它实际上更好,因为你不会冒险让刚刚醒来的读取器立即回到睡眠状态,试图获取写入器仍然持有的锁。

编辑: @DietrichEpp 在评论中提出了一个很好的观点。作者必须以这样一种方式更改状态,以使读者永远无法访问不一致的状态。它可以通过获取条件变量中使用的互斥锁来实现,正如我在上面所指出的,或者通过确保所有状态更改都是原子的。

于 2011-09-24T00:22:50.330 回答
2

这里的基本种族如下所示:

THREAD A        THREAD B
Mutex lock
Check state
                Change state
                Signal
cvar wait
(never awakens)

如果我们在状态变化或信号上或两者上都加锁,那么我们就避免了这种情况;当线程 A 处于其临界区并持有锁时,不可能同时发生状态更改和信号。

如果我们考虑相反的情况,即线程 A 交织到线程 B 中,则没有问题:

THREAD A        THREAD B
                Change state
Mutex lock
Check state
( no need to wait )
Mutex unlock
                Signal (nobody cares)

所以线程 B 没有必要在整个操作中保持互斥锁;它只需要在状态变化和信号之间保持一些可能无限小的间隔的互斥锁。当然,如果状态本身需要锁定以进行安全操作,那么锁定也必须保持在状态更改上。

最后,请注意,在大多数情况下,尽早删除互斥锁不太可能提高性能。要求持有互斥锁减少了条件变量中内部锁的争用,并且在现代 pthreads 实现中,系统可以将等待线程从等待 cvar 转移到等待互斥锁而不唤醒它(从而避免它醒来只是为了立即阻塞互斥体)。 正如评论中所指出的,在某些情况下,删除互斥锁可能会通过减少所需的系统调用数量来提高性能。再一次,它也可能导致对条件变量的内部互斥体的额外争用。很难说。无论如何,这可能不值得担心。

请注意,适用的标准要求pthread_cond_signal在不持有互斥锁的情况下可以安全地调用:

无论线程当前是否拥有调用 pthread_cond_wait() 或 pthread_cond_timedwait() 的线程在等待期间与条件变量相关联的互斥锁,线程都可以调用 pthread_cond_signal() 或 pthread_cond_broadcast() 函数 [...]

这通常意味着条件变量对其内部数据结构具有内部锁,或者使用一些非常谨慎的无锁算法。

于 2011-09-24T00:23:53.687 回答
1

答案是,有一个种族,要消除那个种族,你必须这样做:

/* atomic op outside of mutex, and then: */

pthread_mutex_lock(&m);
pthread_mutex_unlock(&m);

pthread_cond_signal(&c);

数据的保护无关紧要,因为pthread_cond_signal无论如何调用时都不会持有互斥锁。

看,通过锁定和解锁互斥体,您已经创建了一个屏障。在信号器拥有互斥锁的那短暂时刻,可以确定:没有其他线程拥有互斥锁。这意味着没有其他线程正在执行任何关键区域。

这意味着所有线程要么将要让互斥锁发现您发布的更改,要么他们已经找到该更改并带着它跑掉(释放互斥锁),或者没有找到他们正在寻找的并拥有原子地放弃互斥体进入睡眠状态(并保证在条件下很好地等待)。

如果没有互斥锁/解锁,您就没有同步。该信号有时会触发,因为没有看到更改的原子值的线程正在转换到它们的原子睡眠以等待它。

所以这就是互斥锁从发出信号的线程的角度所做的事情。您可以从其他东西获得访问的原子性,但不能获得同步。

PS我之前已经实现过这个逻辑。这种情况发生在 Linux 内核中(使用我自己的互斥锁和条件变量)。

在我的情况下,信号器不可能为共享数据的原子操作保留互斥锁。为什么?因为信号器在用户空间中执行操作,在内核和用户之间共享的缓冲区内,然后(在某些情况下)对内核进行系统调用以唤醒线程。用户空间只需对缓冲区进行一些修改,然后如果满足某些条件,它将执行ioctl.

所以在ioctl通话中我做了互斥锁/解锁的事情,然后点击了条件变量。这确保了线程不会错过与用户空间发布的最新修改相关的唤醒。

起初我只有条件变量信号,但没有互斥体的参与,它看起来是错误的,所以我稍微推理了一下情况,并意识到必须简单地锁定和解锁互斥体以符合同步仪式,从而消除了失去唤醒。

于 2012-03-29T03:39:05.107 回答