C ++ 11线程等待行为:std :: this_thread :: yield()与std :: this_thread :: sleep_for(std :: chrono :: milliseconds(1))

时间:2013-06-26 16:44:26

标签: c++ multithreading c++11 standard-library

在编写Microsoft特定的C ++代码时,我被告知编写Sleep(1)Sleep(0)更好于自旋锁,因为Sleep(0)将占用更多的CPU时间,而且,只有在等待运行的另一个等优先线程时才会产生。

然而,对于C ++ 11线程库,没有太多关于std::this_thread::yield()std::this_thread::sleep_for( std::chrono::milliseconds(1) )的影响的文档(至少我能够找到)。第二个肯定是更冗长,但它们对于自旋锁是否同样有效,或者它是否会受到影响Sleep(0)Sleep(1)的潜在相同的陷阱?

可以接受std::this_thread::yield()std::this_thread::sleep_for( std::chrono::milliseconds(1) )的示例循环:

void SpinLock( const bool& bSomeCondition )
{
    // Wait for some condition to be satisfied
    while( !bSomeCondition )
    {
         /*Either std::this_thread::yield() or 
           std::this_thread::sleep_for( std::chrono::milliseconds(1) ) 
           is acceptable here.*/
    }

    // Do something!
}

4 个答案:

答案 0 :(得分:28)

标准在这里有些模糊,因为具体实现将在很大程度上受底层操作系统的调度功能的影响。

话虽这么说,你可以在任何现代操作系统上安全地假设一些事情:

  • yield将放弃当前时间片并将线程重新插入调度队列。在线程再次执行之前到期的时间通常完全取决于调度程序。请注意,标准将yield称为重新安排的机会。因此,如果需要,实现可以完全自由地从收益中立即返回。 yield永远不会将线程标记为非活动状态,因此在yield上旋转的线程将始终在一个核心上产生100%的负载。如果没有其他线程准备就绪,那么在您再次安排之前,您可能最多会丢失当前时间片的剩余部分。
  • sleep_*将至少阻止线程请求的时间。实施可能会将sleep_for(0)变为yield。另一方面,sleep_for(1)将使您的线程暂停。线程首先进入不同的休眠线程队列,而不是回到调度队列。只有在请求的时间量过去之后,调度程序才会考虑将线程重新插入调度队列。小睡眠产生的负荷仍然很高。如果请求的休眠时间小于系统时间片,则可以预期线程将只跳过一个时间片(即,释放活动时间片然后跳过一个产生一个产量),这仍将导致cpu加载在一个核心上接近甚至等于100%。

关于哪个更适合自旋锁定的几句话。当锁定期望几乎没有争用时,自旋锁定是一种可供选择的工具。如果在绝大多数情况下您希望锁定可用,则自旋锁是一种廉价且有价值的解决方案。但是,只要你有争用,旋转锁花费你。如果你担心产量或睡眠是否是更好的解决方案,那么自旋锁是错误的工具。你应该使用互斥锁。

对于旋转锁定,您实际上必须等待锁定的情况应被视为例外情况。因此,在这里屈服是完全可以的 - 它清楚地表达了意图,并且浪费CPU时间首先应该不是一个问题。

答案 1 :(得分:14)

我刚刚在Windows 7,2.8GHz Intel i7上使用Visual Studio 2013进行了测试,默认发布模式优化。

sleep_for(非零)出现睡眠状态,最小值约为1毫秒,并且在循环中不占用CPU资源,如:

for (int k = 0; k < 1000; ++k)
    std::this_thread::sleep_for(std::chrono::nanoseconds(1));

如果您使用1纳秒,1微秒或1毫秒,则此1,000次睡眠循环大约需要1秒。另一方面,yield()每个大约需要0.25微秒,但会将CPU旋转到100%:

for (int k = 0; k < 4,000,000; ++k) (commas added for clarity)
    std::this_thread::yield();

std :: this_thread :: sleep_for((std :: chrono :: nanoseconds(0))似乎与yield()大致相同(此处未显示测试)。

相比之下,锁定自旋锁的atomic_flag大约需要5纳秒。这个循环是1秒:

std::atomic_flag f = ATOMIC_FLAG_INIT;
for (int k = 0; k < 200,000,000; ++k)
    f.test_and_set();

此外,互斥量约为50纳秒,此循环为1秒:

for (int k = 0; k < 20,000,000; ++k)
    std::lock_guard<std::mutex> lock(g_mutex);

基于此,我可能会毫不犹豫地在自旋锁中加权,但我几乎肯定不会使用sleep_for。如果你认为你的锁会旋转很多而且担心cpu消耗,我会切换到std :: mutex,如果这在你的应用程序中是实用的。希望Windows中std :: mutex表现糟糕的日子已经过去了。

答案 2 :(得分:3)

如果你对使用yield时的cpu负载感兴趣 - 它非常糟糕,除了一个案例 - (只有你的应用程序正在运行,并且你知道它基本上会占用你所有的资源)

这里有更多解释:

  • 在循环中运行yield将确保cpu将释放线程的执行,但是,如果系统尝试返回线程,它将只重复yield操作。这可以使线程使用100%的cpu核心负载。
  • 运行sleep()sleep_for()也是一个错误,这会阻止线程执行但你会在cpu上有等待时间。不要误会,这是工作CPU,但在可能的最低优先级。虽然以某种方式为简单的使用示例工作(完全加载cpu on sleep()是完全加载的工作处理器的一半),如果你想确保应用程序的责任,你会想要第三个例子:
  • 组合! :

    std::chrono::milliseconds duration(1);
    while (true)
       {
          if(!mutex.try_lock())
          {
               std::this_thread::yield();
               std::this_thread::sleep+for(duration);
               continue;
          }
          return;
       }
    

这样的事情将确保,cpu将在执行此操作时产生速度,而sleep_for()将确保cpu在等待一段时间后甚至尝试执行下一次迭代。这个时间当然可以动态(或静态)调整,以满足您的需求

欢呼:)

答案 3 :(得分:1)

您想要的可能是条件变量。具有条件唤醒功能的条件变量通常像您所写的那样实现,在循环内的睡眠或yield会等待条件。

您的代码如下:

std::unique_lock<std::mutex> lck(mtx)
while(!bSomeCondition) {
    cv.wait(lck);
}

std::unique_lock<std::mutex> lck(mtx)
cv.wait(lck, [bSomeCondition](){ return !bSomeCondition; })

您需要做的就是在数据准备就绪时在另一个线程上通知条件变量。但是,如果要使用条件变量,则无法避免在那里锁定。

相关问题