【问题标题】:Cheapest way to wake up multiple waiting threads without blocking唤醒多个等待线程而不阻塞的最便宜方法
【发布时间】:2012-03-20 15:13:29
【问题描述】:

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

现在我使用 boost::condition_variable: 并且所有线程都在 boost::condition_variable::wait() 内部等待调用它们自己的 conditional_variableS 对象。

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

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

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

【问题讨论】:

    标签: c++ multithreading boost synchronization


    【解决方案1】:

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

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

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

    1. 获取互斥体。

    2. 复制状态整数的值。

    3. 阻塞条件变量,释放互斥体。

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

    5. 释放互斥锁。

    要解除对所有线程的阻塞,请执行以下操作:

    1. 获取互斥体。

    2. 增加状态整数。

    3. 广播条件变量。

    4. 释放互斥锁。

    请注意锁定算法的第 4 步如何测试线程是否完成等待?请注意此代码如何跟踪自从线程决定阻塞以来是否有解除阻塞?您必须这样做,因为条件变量自己不会这样做。 (这就是您需要重新获取锁定对象的原因。)

    如果您尝试删除状态整数,您的代码将出现不可预测的行为。有时你会因为错过唤醒而阻塞太久,有时你会因为虚假唤醒而阻塞太久。只有受互斥锁保护的状态整数(或类似谓词)告诉线程何时等待以及何时停止等待。

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

    【讨论】:

    • 谢谢。但这是我现在使用的,但问题是我不想在互斥锁/解锁上花费 CPU 时间。哦...是的,虚假的唤醒...我对它们了解不多。会思考。
    • 您进行了基准测试吗?因为这非常接近最优值。您使用的任何不会让您这样做的机制很可能是因为该机制在内部确实做到了这一点,从而否定了任何可能的节省。
    • 是的,今天使用了 VTune Amplifier XE 2011。有时并行作业太短,线程唤醒需要作业持续时间的 20%。
    • 当你唤醒 N 个线程做一些并行工作时,它怎么能接近最佳状态,而它们都立即做的是争夺同一个互斥锁?
    • @Kaz:因为他们通常最终还是会争夺同一个互斥锁。据推测,线程以某种方式相关,例如同一线程池的一部分。所以他们通常会获得一个锁来检查池的工作队列。让唤醒/阻塞算法了解允许它优化过程。例如,它可以使用等待变形来避免雷鸣般的牛群,而不是实际上让他们都醒来只是为了竞争并重新入睡。 (也就是说,这远非制造雷鸣般的羊群,而是让实现避免了。)
    【解决方案2】:

    对于控制执行并行作业的线程,有一个很好的原语称为屏障。

    一个屏障用一些正整数值 N 初始化,表示它拥有多少个线程。屏障只有一个操作:wait。当N 线程调用等待时,屏障释放所有线程。此外,其中一个线程被赋予一个特殊的返回值,表明它是“串行线程”;该线程将做一些特殊的工作,比如整合来自其他线程的计算结果。

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

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

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

    【讨论】:

    • 由于丢失唤醒问题,我认为没有任何可靠的方法可以唤醒未知数量的线程而没有任何状态。微软多年前以现已过时的函数PulseEvent 的形式尝试过这一点。这仅在协作式多任务 Windows 3 中有效。在单个 CPU 上的协作式任务中,线程不仅知道它是唯一运行的线程,而且所有其他线程实际上都在 yield 函数中挂起。换句话说,is 系统中有一个大的调度互斥锁,因此PulseEvent 是相当条件广播,引用该互斥锁。
    • '唤醒未知数量的没有任何状态的线程' - 我怀疑你是对的,但是有可能唤醒未知数量的线程而这些线程不需要在之后读取状态醒来。关键部分+信号量内的线程数怎么样?需要等待的线程将进入 CS,增加计数,离开 CS 并在信号量上等待。想要发出信号的线程进入 CS,向信号量发出 [count] 个单位的信号,将计数归零并退出 CS。
    • @MartinJames:这将导致虚假唤醒。考虑:没有新的工作要做。线程 A 阻塞。线程 B 对一些工作进行排队并向条件变量发出信号,唤醒线程 A。线程 C 然后完成它正在执行的工作,检查工作队列,找到工作并将其从队列中拉出。最后,线程 B 醒来,发现工作队列为空!哎呀,虚假的唤醒。你如何解决这个问题,而不是毫无意义地强制线程 C 阻塞并等待线程 B 醒来来完成工作——一个比问题更糟糕的解决方案。 (额外的上下文切换、更糟糕的缓存行为等)
    • @MartinJames:您可能认为线程 C 不应该在不更改条件变量状态的情况下更改谓词。但是......条件变量是无状态的。它们的全部意义在于状态由您的代码在外部进行管理。
    • @MartinJames:问题是调用该函数但尚未到达临界区的线程尚未计算在内,因此它们错过了唤醒信号。
    【解决方案3】:

    一般来说,你不能。

    假设算法看起来像这样:

    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()。

    在情况 #1 中,WorkerThread() 回来在 CV 上休眠,并被下一个 cv.notify() 唤醒,一切正常。

    在情况 #2 中,MainThread() 回来并通知... nobody 并继续。与此同时,WorkerThread() 最终回到它的循环中并等待 CV,但它现在是 MainThread() 后面的一个或多个迭代。

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

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

    虚假唤醒是两件事:

    • 仔细编写您的程序,并确保即使您 错过了什么。
    • 支持高效的 SMP 实施

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

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

    【讨论】:

      【解决方案4】:

      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()。

      【讨论】:

        猜你喜欢
        • 2017-07-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-03-18
        • 1970-01-01
        • 2018-10-13
        • 1970-01-01
        • 2020-08-31
        相关资源
        最近更新 更多