繁体   English   中英

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

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

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

但是,对于 C++11 线程库,关于std::this_thread::yield()std::this_thread::sleep_for( std::chrono::milliseconds(1) )的影响的文档并不多(至少我能找到) 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!
}

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

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

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

关于哪个更适合自旋锁定的几句话。 当期望很少或没有锁争用时,自旋锁定是一种选择的工具。 如果在绝大多数情况下您希望锁可用,则自旋锁是一种廉价且有价值的解决方案。 但是,一旦您确实有争用,自旋锁就会花费您。 如果您担心 yield 或 sleep 是否是更好的解决方案,那么自旋锁是该工作的错误工具 您应该改用互斥锁。

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

我刚刚在 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 性能非常糟糕的日子已经过去了。

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

欢呼:)

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM