条件变量 vs 信号量

158

何时使用信号量(semaphore),何时使用条件变量(conditional variable)?


1
相关信息也可以在链接https://dev59.com/DG855IYBdhLWcg3w_5qm中找到。 - Karthik Balaguru
8个回答

264

锁被用于互斥。当您想要确保一段代码是原子性的时候,请在其周围放置一个锁。理论上,您可以使用二进制信号量来实现此目的,但那是一种特殊情况。

信号量和条件变量基于锁提供的互斥性,并用于提供对共享资源的同步访问。它们可用于类似的目的。

条件变量通常用于避免忙等待(重复循环检查条件),同时等待资源变得可用。例如,如果您有一个线程(或多个线程)无法继续前进,直到队列为空,则忙等待的方法将只做以下事情:

//pseudocode
while(!queue.empty())
{
   sleep(1);
}

这样做的问题是,通过让该线程重复检查条件,你浪费了处理器时间。为什么不反而使用一个同步变量,可以通过发送信号告诉线程该资源可用呢?

//pseudocode
syncVar.lock.acquire();

while(!queue.empty())
{
   syncVar.wait();
}

//do stuff with queue

syncVar.lock.release();

假设你已经在其他地方有一个线程正在从队列中取出物品。 当队列为空时,可以调用syncVar.signal()来唤醒一个正处于syncVar.wait()睡眠状态下的随机线程(或通常还会有一个signalAll()broadcast()方法来唤醒所有正在等待的线程)。

我通常在一个或多个线程等待单个特定条件(例如等待队列为空)时使用同步变量。

信号量可以类似地使用,但我认为它们更适合用于共享资源,该资源基于某些可用事物的整数数量而可用或不可用。信号量对于生产者/消费者情况很有用,其中生产者正在分配资源,而消费者正在消耗这些资源。

想象一下如果你有一台汽水自动售货机。只有一个汽水机,它是一个共享资源。你有一个负责保持机器进货的卖家(生产者)线程和N个想要从机器中取出汽水的买家(消费者)线程。 机器中的汽水数量是驱动我们的信号量的整数值。

每个买家(消费者)线程都会调用信号量的down()方法来取汽水。这将从机器中取出汽水并将可用汽水数量减少1. 如果有汽水可用,代码将继续运行过down()语句而不会出现问题。如果没有汽水可用,则线程将在此处休眠,等待在有更多汽水可用时被通知(当机器中有更多汽水时)。

卖家(生产者)线程实际上将等待汽水机为空。当最后一瓶汽水被从机器中取出时(可能有一个或多个消费者正在等待取出汽水),卖家将得到通知。卖家将使用信号量的up()方法重新进货,每次可用汽水的数量都会增加,等待的消费者线程将得到通知有更多汽水可用。

同步变量的wait()signal()方法往往隐藏在信号量的down()up()操作中。

当然,两种选择之间存在重叠。许多情况下,信号量或条件变量(或一组条件变量)都可以满足您的需求。信号量和条件变量都与它们用于维护互斥性的锁定对象相关联,但是它们在锁定的基础上提供了额外的功能以同步线程执行。在大多数情况下,由您来确定哪种方法最适合您的情况。

这可能不是最技术性的描述,但在我的脑海中这就是它有意义的方式。


18
很棒的答案,我想从其他答案中补充一下:信号量用于控制执行线程的数量。将有一组固定的资源,每当一个线程拥有同样的资源时,资源计数将递减。当信号量计数达到0时,不允许其他线程获取资源。线程被阻塞,直到其他拥有资源的线程释放它们。简而言之,主要区别在于允许同时获取资源的线程数量?互斥量——只有一个。信号量——它是定义的数量(与信号量计数一样多)。 - berkay
17
这个while循环存在的原因是为了避免所谓的“虚假唤醒”现象,而不是简单的if语句。根据维基百科上的描述:"导致这种情况的原因之一是虚假唤醒,即一个线程可能会从其等待状态中被唤醒,尽管没有任何线程发出条件变量的信号。" - Vladislavs Burakovs
4
好的!我认为这也有助于以下情况:广播唤醒的线程数超出了可用资源的数量(例如,广播唤醒3个线程,但队列中只有2个项目)。 - Brent Writes Code
4
为了澄清一下,一个刚被唤醒的线程可能仍然会发现条件为假(导致虚假的唤醒),原因是在该线程有机会重新检查条件之前,可能已经发生了上下文切换,在此期间,一些其他已安排的线程使得该条件变为假。这是我所知道的虚假唤醒的一个原因,不知道还有没有其他的。 - max
我发现并不完全清楚为什么syncVar需要一个独立可访问的锁,即为什么线程不能简单地执行syncVar.wait(),并在条件满足且线程具有独占访问权限时返回。当完成后,它可以syncVar().release() - einpoklum
显示剩余3条评论

68

让我们揭开其本质。

条件变量本质上是一个等待队列,它支持阻塞等待和唤醒操作。也就是说,您可以将线程放入等待队列中,并将其状态设置为BLOCK,也可以从中获取线程并将其状态设置为READY。

请注意,要使用条件变量,需要两个其他元素:

  • 条件(通常通过检查标志或计数器实现)
  • 保护条件的互斥锁

然后协议变成了:

  1. 获得互斥锁
  2. 检查条件
  3. 如果条件为真,则阻塞并释放互斥锁,否则释放互斥锁

信号量本质上是一个计数器 + 互斥锁 + 等待队列。它可以独立使用而不需要外部依赖。您可以将其用作互斥锁或条件变量。

因此,信号量可以被视为比条件变量更复杂的结构,而后者更轻巧和灵活。


互斥锁可以被视为条件变量,它的条件是是否被持有。 - 宏杰李
协议的描述是错误的! - John

23

信号量可以用来实现对变量的独占访问,但是它们的意图是用于同步。而互斥锁的语义严格与互斥有关:只有锁定资源的进程可以解锁它。

不幸的是,互斥锁无法实现同步,这就是为什么我们需要条件变量。另外请注意,使用条件变量时,您可以通过广播解锁所有等待线程,使它们在同一时刻被解锁。这不能通过信号量实现。


12

信号量和条件变量非常相似,通常用于相同的目的。然而,有一些微小的区别可能会使一个更可取。例如,要实现屏障同步,您将无法使用信号量。但条件变量是理想的。

屏障同步是当您想让所有线程等待,直到每个人都到达线程函数中的某个部分时。这可以通过具有静态变量来实现,该变量最初为总线程数的值,并在每个线程到达该屏障时递减。这意味着我们希望每个线程在最后一个线程到达之前都处于休眠状态。信号量会恰好相反!使用信号量,每个线程都将继续运行,而最后一个线程(它将将信号量值设置为0)将进入睡眠状态。

另一方面,条件变量是理想的。当每个线程到达屏障时,我们检查静态计数器是否为零。如果不是,则使用条件变量等待功能将线程设置为睡眠状态。当最后一个线程到达屏障时,计数器值将递减为零,此最后一个线程将调用条件变量信号功能,这将唤醒所有其他线程!


条件变量并不适合实现屏障。特别是,在线程减少计数器并在条件变量上休眠之间存在竞争条件。因此,还需要一个互斥锁。每个线程必须先获取互斥锁,然后减少和检查计数器,然后在原子释放互斥锁的同时在条件变量上休眠。当稍后所有线程都醒来时,它们都需要重新获取该互斥锁,但只能一次执行一个线程。因此,如果操作系统库提供了屏障原语,则使用它! - Kai Petzke

1

信号量需要在初始化时提前知道计数。条件变量没有这样的要求。


1

我在监视器同步下对文件条件变量进行分类。我通常将信号量和监视器视为两种不同的同步风格。两者之间存在差异,包括固有状态数据的保存量以及如何模拟代码 - 但是实际上,没有一种方法可以解决问题而另一种方法不能解决。

我倾向于编写监视器形式的代码; 在我工作的大多数语言中,这归结为互斥锁,条件变量和一些后端状态变量。但是信号量也能完成任务。


3
如果您解释一下“监测表格”是什么,那么这将是一个更好的答案。 - Steven Lu

-1

mutex条件变量都是从信号量继承而来。

  • 对于mutex信号量使用两个状态:0、1
  • 对于条件变量信号量使用计数器。

它们就像语法糖一样。


1
在C++标准库中,它们都是独立的对象,使用特定于平台的API实现。信号量肯定会解除已发出的次数,条件变量可能会被多次发出信号,但只会解除一次。这就是为什么wait函数需要一个互斥锁作为参数的原因。 - doron

-2

条件变量 + 互斥锁 == 信号量


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接