66

我确信互斥锁是不够的,这就是条件变量概念存在的原因;但它打败了我,当条件变量必不可少时,我无法用具体的场景说服自己。

条件变量、互斥锁和锁之间的差异问题的接受答案说条件变量是

用“信号”机制锁定。当线程需要等待资源可用时使用它。线程可以在 CV 上“等待”,然后资源生产者可以“向”变量发出“信号”,在这种情况下,等待 CV 的线程会收到通知并可以继续执行

我感到困惑的是,一个线程也可以在互斥体上等待,当它收到信号时,仅仅意味着该变量现在可用,为什么我需要一个条件变量?

PS:另外,无论如何,都需要一个互斥锁来保护条件变量,这让我的视线更加偏向于看不到条件变量的目的。

4

6 回答 6

40

即使您可以按照您描述的方式使用它们,互斥锁也不是为用作通知/同步机制而设计的。它们旨在提供对共享资源的互斥访问。使用互斥体来发出条件信号很尴尬,我想看起来像这样(其中 Thread1 由 Thread2 发出信号):

线程1:

while(1) {
    lock(mutex); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex); // Tells Thread2 we are done
}

线程2:

while(1) {
    ... // do the work that precedes notification
    unlock(mutex); // unblocks Thread1
    lock(mutex); // lock the mutex so Thread1 will block again
}

这有几个问题:

  1. Thread2 不能继续“做通知之前的工作”,直到 Thread1 完成“通知后的工作”。有了这种设计,Thread2 甚至都不是必需的,也就是说,为什么不将“工作在前”和“工作在通知后”移到同一个线程中,因为在给定的时间只能运行一个!
  2. 如果 Thread2 不能抢占 Thread1,Thread1 将在重复 while(1) 循环时立即重新锁定互斥锁,即使没有通知,Thread1 也会继续执行“通知后工作”。这意味着您必须以某种方式保证 Thread2 将在 Thread1 之前锁定互斥锁。你是怎样做的?也许通过睡眠或其他一些特定于操作系统的方式强制调度事件,但即使这也不能保证根据时间、操作系统和调度算法工作。

这两个问题都不是小问题,事实上,它们都是主要的设计缺陷和潜在的错误。这两个问题的根源是互斥锁在同一个线程中被锁定和解锁的要求。那么如何避免上述问题呢?使用条件变量!

顺便说一句,如果您的同步需求真的很简单,您可以使用一个普通的旧信号量来避免条件变量的额外复杂性。

于 2012-09-24T16:41:03.333 回答
11

互斥是共享资源的独占访问,而条件变量是关于等待条件为真。两者都是不同的内核资源。有些人可能认为他们可以使用互斥锁自己实现条件变量,常见的模式是“标志+互斥锁”:

lock(mutex)

while (!flag) {
    sleep(100);
}

unlock(mutex)

do_something_on_flag_set();

但它永远不会起作用,因为您在等待期间永远不会释放互斥锁,没有其他人可以以线程安全的方式设置标志。这就是为什么我们需要条件变量的原因,当您等待条件变量时,相关联的互斥锁不会被您的线程持有,直到它发出信号。

于 2014-04-30T11:28:39.653 回答
7

我也在考虑这个问题,我认为到处都缺少的最重要的信息是互斥锁一次只能由一个线程拥有(或更改)。因此,如果您有一个生产者和多个消费者,则生产者将不得不等待互斥体进行生产。与条件。它可以随时产生的变量。

于 2014-12-29T18:48:30.927 回答
4

您需要条件变量,与互斥锁(每个 cond.var. 属于互斥锁)一起使用,以指示从一个线程到另一个线程的状态(条件)变化。这个想法是线程可以等到某个条件变为真。这些条件是特定于程序的(即“队列为空”、“矩阵很大”、“某些资源几乎耗尽”、“某些计算步骤已完成”等)。一个互斥锁可能有几个相关的条件变量。而且您需要条件变量,因为这些条件可能并不总是简单地表示为“互斥锁已锁定”(因此您需要将条件更改广播到其他线程)。

阅读一些好的 posix 线程教程,例如本教程那个那个。更好的是,阅读一本好的 pthread 书籍。看到这个问题

另请阅读高级 Unix 编程高级 Linux 编程

PS 并行和线程是难以掌握的概念。花时间阅读和实验,然后再阅读。

于 2012-09-23T10:07:19.193 回答
3

条件变量和互斥锁对可以替换为二进制信号量和互斥锁对。使用条件 var + mutex 时,消费者线程的操作顺序为:

  1. 锁定互斥锁

  2. 等待条件变量

  3. 过程

  4. 解锁互斥锁

生产者线程的操作顺序是

  1. 锁定互斥锁

  2. 向条件变量发出信号

  3. 解锁互斥锁

使用 sema+mutex 对时对应的消费者线程序列为

  1. 等待二进制 sema

  2. 锁定互斥锁

  3. 检查预期条件

  4. 如果条件为真,则处理。

  5. 解锁互斥锁

  6. 如果步骤 3 中的条件检查为假,则返回步骤 1。

生产者线程的顺序是:

  1. 锁定互斥锁

  2. 发布二进制 sema

  3. 解锁互斥锁

如您所见,使用条件 var 时步骤 3 中的无条件处理被使用二进制 sema 时步骤 3 和步骤 4 中的条件处理替换。

原因是当使用 sema+mutex 时,在竞争条件下,另一个消费者线程可能会潜入第 1 步和第 2 步之间并处理/取消条件。使用条件变量时不会发生这种情况。使用条件 var 时,在第 2 步之后保证条件为真。

二进制信号量可以替换为常规计数信号量。这可能会导致步骤 6 到步骤 1 循环多次。

于 2016-04-17T22:06:39.213 回答
0

我认为它是实现定义的。
互斥体是否足够取决于您是否将互斥体视为临界区的机制或更多。

http://en.cppreference.com/w/cpp/thread/mutex/unlock中所述,

互斥锁必须由当前执行线程锁定,否则行为未定义。

这意味着线程只能解锁在 C++ 中被自己锁定/拥有的互斥锁。
但在其他编程语言中,您可能能够在进程之间共享互斥锁。

因此区分这两个概念可能只是性能考虑,复杂的所有权标识或进程间共享对于简单的应用程序是不值得的。


例如,您可以使用额外的互斥锁修复 @slowjelj 的情况(这可能是不正确的修复):

线程1:

lock(mutex0);
while(1) {
    lock(mutex0); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex1); // Tells Thread2 we are done
}

线程2:

while(1) {
    lock(mutex1); // lock the mutex so Thread1 will block again
    ... // do the work that precedes notification
    unlock(mutex0); // unblocks Thread1
}

但是您的程序会抱怨您触发了编译器留下的断言(例如,Visual Studio 2015 中的“解锁无主互斥锁”)。

于 2017-05-26T10:02:06.607 回答