C++11线程等待行为:std::this_Thread::yield()与std::this _Thread::sle

C++11 Thread waiting behaviour: std::this_thread::yield() vs. std::this_thread::sleep_for( std::chrono::milliseconds(1) )

本文关键字:std Thread this sle yield 线程 C++11 等待      更新时间:2023-10-16

在编写特定于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)的潜在相同gotchas的影响?

可以接受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!
}

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

话虽如此,你可以放心地在任何现代操作系统上假设一些事情:

  • CCD_ 10将放弃当前时间片并将线程重新插入到调度队列中。线程再次执行之前到期的时间量通常完全取决于调度程序。请注意,标准将收益率称为重新安排的机会。因此,如果一个实现想要的话,它可以完全自由地立即从收益中返回。产量永远不会将线程标记为无效,因此在产量上旋转的线程将始终在一个核心上产生100%的负载。如果没有其他线程准备好,那么在重新安排之前,您可能最多会损失当前时间片的剩余时间
  • CCD_ 11将阻塞线程至少所请求的时间量。一种实现方式可以将sleep_for(0)转变为yield。另一方面,sleep_for(1)将使您的线程处于挂起状态。线程不是返回到调度队列,而是先转到另一个睡眠线程队列。只有在经过了请求的时间后,调度程序才会考虑将线程重新插入到调度队列中。小睡眠产生的负荷仍然很高。如果请求的睡眠时间小于系统时间片,则可以预期线程将只跳过一个时间片(即,一次释放活动时间片,然后跳过该时间片),这仍然会导致一个内核上的cpu负载接近甚至等于100%

关于哪种更适合旋转锁定,请简单介绍。当期望锁上几乎没有争用时,旋转锁定是一种选择工具。如果在绝大多数情况下,你希望锁是可用的,那么旋转锁是一个廉价而有价值的解决方案。但是,一旦发生争用,旋转锁就会让您付出的代价。如果你担心产量还是睡眠是更好的解决方案,那么旋转锁是错误的工具。您应该使用互斥。

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

我刚刚在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毫秒,1000次睡眠的循环大约需要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::纳秒(0))似乎与yield()大致相同(此处未显示测试)。

相比之下,为spinlock锁定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);

基于此,我可能会毫不犹豫地在spinlock中加入yield,但我几乎肯定不会使用sleep_for。如果你认为你的锁会旋转很多,并且担心cpu消耗,如果在你的应用程序中可行的话,我会切换到std::mutex。希望Windows中std::mutex性能非常糟糕的日子已经过去了。

如果你在使用yield时对cpu负载感兴趣,那就很糟糕了,只有一种情况除外(只有你的应用程序在运行,你知道它基本上会吃掉你所有的资源)

这里有更多的解释:

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

    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在尝试执行下一次迭代之前等待一段时间。这个时间当然可以动态(或静态)调整,以满足您的需求

cheers:)

您想要的可能是一个条件变量。带有条件唤醒函数的条件变量通常像您正在编写的那样实现,其中sleep或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; })

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