条件变量与信号量

dor*_*ron 92 multithreading synchronization operating-system semaphore mutual-exclusion

什么时候应该使用信号量,什么时候应该使用条件变量(CondVar)?

Bre*_*ode 187

锁用于互斥.如果要确保一段代码是原子的,请锁定它.理论上你可以使用二进制信号量来做到这一点,但这是一个特例.

信号量和条件变量建立在互锁的基础上,由锁提供,用于提供对共享资源的同步访问.它们可用于类似目的.

条件变量通常用于在等待资源变为可用时避免繁忙等待(在检查条件时重复循环).例如,如果你有一个线程(或多个线程)在队列为空之前无法继续,那么繁忙的等待方法就是执行以下操作:

//pseudocode
while(!queue.empty())
{
   sleep(1);
}
Run Code Online (Sandbox Code Playgroud)

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

//pseudocode
syncVar.lock.acquire();

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

//do stuff with queue

syncVar.lock.release();
Run Code Online (Sandbox Code Playgroud)

据推测,你会在其他地方有一个线程将事情从队列中拉出来.当队列为空时,它可以调用syncVar.signal()唤醒一个正在睡眠的随机线程syncVar.wait()(或者通常还有一个signalAll()broadcast()方法来唤醒所有正在等待的线程).

当我有一个或多个线程等待单个特定条件时(例如,队列为空),我通常使用这样的同步变量.

信号量可以类似地使用,但我认为当你拥有一个可用且基于某些整数可用事物而不可用的共享资源时,它们会被更好地使用.信号量对于生产者/消费者的情况是有益的,其中生产者正在分配资源并且消费者正在消费它们.

想想你是否有自动售货机.只有一台汽水机,它是一个共享资源.你有一个线程是供应商(生产者)负责保持机器库存,N个线程是买家(消费者)想要从机器中取出苏打水.机器中的苏打水数是驱动我们信号量的整数值.

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

供应商(生产者)线程基本上是在等待苏打水机为空.当从机器取出最后一个苏打水时,供应商会收到通知(并且一个或多个消费者可能正在等待苏打水消失).供应商将使用信号量up()方法补充苏打水机器,每次可用的苏打水数量将增加,从而等待的消费者线程将被通知更多的苏打水可用.

wait()signal()同步变量的方法趋向于被所述内隐藏down()up()信号量的操作.

当然,这两种选择之间存在重叠.在许多情况下,信号量或条件变量(或条件变量集)都可以满足您的需要.信号量和条件变量都与用于维护互斥的锁定对象相关联,但随后它们在锁定之上提供额外的功能以同步线程执行.这主要取决于你找出哪一个对你的情况最有意义.

这不一定是最技术性的描述,但这就是我脑子里的理解.

  • 只是详细说明为什么有这个while循环而不是简单的if:叫做'spurios wakeup**'的东西.引用[这个维基百科文章](http://en.wikipedia.org/wiki/Spurious_wakeup):_"其中一个原因是虚假的唤醒;也就是说,一个线程可能会从其等待状态唤醒,即使没有线程信号条件变量"_ (9认同)
  • 很好的答案,我想补充其他答案:信号量用于控制执行的线程数.将有固定的资源集.每当线程拥有相同的资源时,资源计数就会减少.当信号量计数达到0时,则不允许其他线程获取资源.线程被阻塞,直到其他线程拥有资源释放.简而言之,主要区别在于允许一次获取资源的线程数量是多少?互斥 - 一个.信号量 - 它的DEFINED_COUNT,(与信号量一样多) (8认同)
  • 哇!!很好的答案. (7认同)
  • @VladislavsBurakovs好点!我认为这对于广播唤醒比可用资源更多线程的情况也是有帮助的(例如,广播唤醒3个线程,但队列中只有2个项目). (3认同)
  • @VladislavsBurakovs为了澄清一点,对于刚刚唤醒的线程(导致虚假唤醒),条件可能仍然是错误的原因是在线程有机会检查条件之前可能已经有上下文切换再次,其他一些预定的线程使该条件为假.这是我知道虚假唤醒的一个原因,不知道是否还有更多. (2认同)

小智 41

让我们揭示一下幕后的内容.

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

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

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

然后协议成为,

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

信号量本质上是一个计数器+一个互斥锁+一个等待队列.它可以在没有外部依赖性的情况下使用.您可以将其用作互斥锁或条件变量.

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


Dac*_*cav 15

信号量可用于实现对变量的独占访问,但它们用于同步.另一方面,互斥锁具有与互斥完全相关的语义:只有锁定资源的进程才能解锁.

遗憾的是,您无法实现与互斥锁的同步,这就是我们拥有条件变量的原因.另请注意,使用条件变量,您可以使用广播解锁在同一时刻解锁所有等待的线程.使用信号量无法做到这一点.


小智 6

信号量和条件变量非常相似,主要用于相同的目的.但是,有一些细微差别可能使一个人更可取.例如,要实现屏障同步,您将无法使用信号量.但条件变量是理想的.

屏障同步是指您希望所有线程都等到每个人都到达线程函数中的某个部分.这可以通过使用静态变量来实现,该静态变量最初是每个线程在到达该障碍时递减的总线程的值.这意味着我们希望每个线程一直睡到最后一个线程到达.信号量就会完全相反!使用信号量,每个线程将继续运行,最后一个线程(将信号量值设置为0)将进入休眠状态.

另一方面,条件变量是理想的.当每个线程到达屏障时,我们检查我们的静态计数器是否为零.如果没有,我们使用条件变量wait函数将线程设置为sleep.当最后一个线程到达屏障时,计数器值将减少到零,最后一个线程将调用条件变量信号函数,它将唤醒所有其他线程!