在编写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!
}
答案 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负载感兴趣 - 它非常糟糕,除了一个案例 - (只有你的应用程序正在运行,并且你知道它基本上会占用你所有的资源)
这里有更多解释:
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; })
您需要做的就是在数据准备就绪时在另一个线程上通知条件变量。但是,如果要使用条件变量,则无法避免在那里锁定。