43

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

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

4 回答 4

36

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

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

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

关于哪个更适合自旋锁定的几句话。当对锁几乎没有争用时,自旋锁是一种选择的工具。如果在绝大多数情况下您希望锁可用,则自旋锁是一种便宜且有价值的解决方案。但是,一旦发生争用,自旋锁就会让您付出代价。如果您担心 yield 或 sleep 是更好的解决方案,这里自旋锁是错误的工作工具。您应该改用互斥锁。

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

于 2013-06-26T20:40:11.263 回答
17

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

sleep_for(nonzero) 至少会出现大约一毫秒的睡眠,并且在循环中不占用 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);

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

于 2016-02-14T07:10:25.467 回答
5

如果您在使用 yield 时对 cpu 负载感兴趣 - 这非常糟糕,除了一种情况 - (只有您的应用程序正在运行,并且您知道它基本上会吃掉您的所有资源)

这里有更多解释:

  • 在循环中运行 yield 将确保 cpu 将释放线程的执行,但是,如果系统尝试返回线程,它只会重复 yield 操作。这可以使线程使用 100% 的 CPU 核心负载。
  • 运行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 在尝试执行下一次迭代之前等待一段时间。这个时间当然可以动态(或静态)调整以满足您的需求

干杯:)

于 2014-11-03T19:37:52.993 回答
4

你想要的可能是一个条件变量。具有条件唤醒功能的条件变量通常像您正在编写的那样实现,循环内的 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; })

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

于 2019-01-29T03:19:43.617 回答