Answers:
锁用于互斥。当您想要确保一段代码是原子的时,请在其周围加一个锁。从理论上讲,您可以使用二进制信号量来执行此操作,但这是一种特殊情况。
信号量和条件变量建立在锁提供的互斥之上,用于提供对共享资源的同步访问。它们可以用于类似目的。
通常使用条件变量来避免在等待资源可用时的繁忙等待(在检查条件时反复循环)。例如,如果您有一个(或多个)线程在队列为空之前无法继续运行,那么繁忙的等待方法将是执行以下操作:
//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()
or broadcast()
方法来唤醒所有正在等待的线程)。
当我有一个或多个线程在单个特定条件下等待时(例如,队列为空),我通常使用这样的同步变量。
信号量可以类似地使用,但是我认为当您拥有一个共享资源时,基于一些可用事物的整数,它们可以更好地使用。信号量适用于生产者/消费者的情况,生产者正在分配资源而消费者正在消耗它们。
考虑一下您是否有汽水自动售货机。只有一台汽水机,这是一种共享资源。您有一个线程是供应商(生产者),负责保持机器的库存,有N个线程是买家(消费者),他们希望从机器中取出苏打水。机器中的苏打水数量是将驱动信号量的整数值。
苏打水机器上的每个购买者(消费者)线程都会调用信号量down()
方法来获取苏打水。这将从机器中获取苏打水,并将可用苏打水的数量减少1。如果有苏打水可用,则代码将继续运行,直到通过down()
语句而没有问题。如果没有可用的苏打水,线程将在此处休眠,等待再次提供苏打水时通知您(当机器中有更多苏打水时)。
供应商(生产者)线程实际上将在等待苏打水机器为空。当从机器中取出最后的苏打水时,供应商会收到通知(一个或多个消费者可能正在等待将苏打水取出)。供应商将使用信号量up()
方法对苏打水机器进行补货,每次都会增加苏打水的可用数量,从而等待的消费者线程将收到更多苏打水可用的通知。
同步变量的wait()
和signal()
方法往往隐藏在信号量的down()
和up()
操作中。
当然,这两个选择之间存在重叠。在许多情况下,信号量或条件变量(或条件变量集)都可以满足您的目的。信号量和条件变量都与用于保持互斥的锁定对象相关联,但随后它们在锁定之上提供了用于同步线程执行的额外功能。由您自己决定哪种情况最适合您的情况。
那不一定是最技术性的描述,但这就是我的脑海。
让我们来揭开背后的秘密。
条件变量本质上是一个等待队列,它支持阻塞等待和唤醒操作,即,您可以将线程放入等待队列并将其状态设置为BLOCK,然后从中取出线程并将其状态设置为READY。
请注意,要使用条件变量,还需要两个其他元素:
协议就变成了
信号量实际上是一个计数器+一个互斥体+一个等待队列。而且可以直接使用而无需外部依赖。您可以将其用作互斥量或条件变量。
因此,与条件变量相比,信号量可以被视为更复杂的结构,而条件变量则更为轻巧和灵活。
信号量和条件变量非常相似,并且大多数用于相同目的。但是,有一些细微的差异可能会使其更可取。例如,要实现屏障同步,您将无法使用信号量,但是条件变量是理想的选择。
屏障同步是当您希望所有线程都等到每个人都到达线程函数中的某个部分时。这可以通过具有一个静态变量来实现,该变量最初是每个线程到达该屏障时所减少的总线程值。这意味着我们要让每个线程都睡到最后一个线程到来为止。信号量的作用恰恰相反!使用信号量,每个线程将继续运行,最后一个线程(将信号量值设置为0)将进入睡眠状态。
另一方面,条件变量是理想的。当每个线程到达障碍时,我们都会检查静态计数器是否为零。如果不是,我们使用条件变量wait函数将线程设置为休眠状态。当最后一个线程到达屏障时,计数器值将减小为零,并且最后一个线程将调用条件变量信号函数,该函数将唤醒所有其他线程!