最简单的方法来唤醒多个等待线程而不会阻塞

时间:2012-03-20 15:13:29

标签: c++ multithreading boost synchronization

我使用boost :: thread来管理线程。在我的程序中,我有一些线程(工作者),有时会被激活以同时完成某项工作。

现在我使用boost :: condition_variable:并且所有线程都在自己的conditional_variableS对象上调用boost :: condition_variable :: wait()调用。

当我使用conditional_variables时,我可以在经典方案中使用互斥锁吗?我想唤醒线程,但不需要将一些数据传递给它们,因此在唤醒过程中不需要锁定/解锁互斥锁,为什么我要花费CPU(但是,我应该记住虚假的唤醒)?

当CV收到通知时,boost :: condition_variable :: wait()调用尝试REACQUIRE锁定对象。但我不需要这个确切的设施。

从另一个线程中唤醒多个线程的最便宜的方法是什么?

4 个答案:

答案 0 :(得分:3)

如果你没有重新获取锁定对象,那么线程如何知道它们已经完成了等待?会告诉他们什么?从块返回告诉他们什么都没有,因为阻塞对象是无状态的。它没有“未锁定”或“不阻塞”状态才能返回。

你必须将一些数据传递给他们,否则在他们不得不等待之前他们怎么知道呢?现在他们不知道呢?条件变量完全是无状态的,因此您需要的任何状态都必须由您维护和传递。

一种常见模式是使用互斥锁,条件变量和状态整数。要阻止,请执行以下操作:

  1. 获取互斥锁。

  2. 复制状态整数的值。

  3. 阻止条件变量,释放互斥锁。

  4. 如果状态整数与您处理时的状态整数相同,请转到步骤3.

  5. 释放互斥锁。

  6. 要取消阻止所有线程,请执行以下操作:

    1. 获取互斥锁。

    2. 递增状态整数。

    3. 广播条件变量。

    4. 释放互斥锁。

    5. 请注意锁定算法的第4步如何测试线程是否已完成等待?请注意,此代码如何跟踪线程是否已阻止,因为线程决定阻止?你必须这样做,因为条件变量本身不会这样做。 (这就是你需要重新获取锁定对象的原因。)

      如果您尝试删除状态整数,则代码将无法预测。有时你会因为错过唤醒而阻塞太长时间,有时你会因为虚假的唤醒而无法阻挡足够长的时间。只有受互斥锁保护的状态整数(或类似谓词)才能告诉线程何时等待以及何时停止等待。

      另外,我还没有看到你的代码如何使用它,但它几乎总是折叠成你已经使用的逻辑。为什么线程会阻塞?是因为他们没有工作要做吗?当他们醒来时,他们会弄清楚该做什么吗?好吧,发现他们没有工作要做,并找出他们需要做的工作需要一些锁,因为它是共享状态,对吧?因此,当您决定阻止并且在等待时需要重新获取时,几乎总是存在锁定。

答案 1 :(得分:0)

为了控制执行并行作业的线程,有一个很好的原语叫做屏障。

使用一些正整数值N初始化屏障,表示它保存的线程数。屏障只有一个操作:wait。当N个线程调用wait时,屏障会释放所有这些线程。另外,其中一个线程被赋予一个特殊的返回值,表明它是“串行线程”;该线程将是一个做一些特殊工作的线程,比如从其他线程集成计算结果。

限制是给定的障碍必须知道确切的线程数。它非常适合并行处理类型的情况。

POSIX在2003年增加了障碍。网络搜索表明Boost也有这些障碍。

http://www.boost.org/doc/libs/1_33_1/doc/html/barrier.html

答案 2 :(得分:0)

一般来说,你不能。

假设算法看起来像这样:

ConditionVariable cv;

void WorkerThread()
{
  for (;;)
  {
    cv.wait();
    DoWork();
  }
}

void MainThread()
{
  for (;;)
  {
    ScheduleWork();
    cv.notify_all();
  }
}

注意:我故意在此伪代码中省略对互斥锁的任何引用。出于本示例的目的,我们假设ConditionVariable不需要互斥锁。

第一次通过MainTnread(),工作排队,然后它通知WorkerThread()它应该执行它的工作。在这一点上可能会发生两件事:

  1. WorkerThread()在MainThread()完成ScheduleWork()之前完成DoWork()。
  2. MainThread()在WorkerThread()完成DoWork()之前完成ScheduleWork()。
  3. 在#1的情况下,WorkerThread()回来在CV上睡觉,并被下一个cv.notify()唤醒,一切都很顺利。

    如果是#2,MainThread()会回来并通知...... nobody 并继续。同时,WorkerThread()最终在其循环中返回并等待CV,但它现在是MainThread()后面的一次或多次迭代。

    这被称为“失去的唤醒”。它类似于臭名昭着的“虚假唤醒”,因为两个线程现在对发生了多少notify()s有不同的想法。如果您期望两个线程保持同步(通常是您),则需要某种共享同步原语来控制它。这就是互斥体的用武之地。它有助于避免失去唤醒,这可能是一个比虚假变种更严重的问题。无论哪种方式,效果都可能很严重。

    更新:有关此设计背后的进一步说明,请参阅原始POSIX作者之一的评论:https://groups.google.com/d/msg/comp.programming.threads/cpJxTPu3acc/Hw3sbptsY4sJ

      

    虚假唤醒有两件事:

         
        
    • 仔细编写您的程序,即使您确定它也能正常工作    错过了什么。
    •   
    • 支持高效的SMP实施
    •   
         

    可能有少数情况下“绝对,偏执地正确”   条件唤醒的实现,同时等待和   信号/广播在不同的处理器上,需要额外的   同步会减慢所有条件变量操作   在99.99999%的通话中没有提供任何好处。值得吗?   高架?没办法!

         

    但是,真的,这是一个借口,因为我们想强迫人们去   写安全代码。 (是的,这是事实。)

答案 3 :(得分:0)

boost :: condition_variable :: notify _ *(lock) NOT 要求调用者保持对互斥锁的锁定。这是对Java模型的一个很好的改进,因为它通过持有锁来解除线程的通知。

严格来说,这意味着以下无意义的代码应该按照您的要求进行:

lock_guard lock(mutex);
// Do something
cv.wait(lock);
// Do something else


unique_lock otherLock(mutex);
//do something
otherLock.unlock();
cv.notify_one();

我认为你不需要先调用otherLock.lock()。