自旋锁与 std::mutex::try_lock
Spinlock vs std::mutex::try_lock
使用专门设计的自旋锁(例如 http://anki3d.org/spinlock)与这样的代码相比有什么好处:
std::mutex m;
while (!m.try_lock()) {}
# do work
m.unlock();
在典型的硬件上,有很多好处:
您的天真 "fake spinlock" 可能会在 CPU 自旋时使内部 CPU 总线饱和,从而使其他物理内核(包括持有锁的物理内核)处于饥饿状态。
如果 CPU 支持超线程或类似的东西,你的天真 "fake spinlock" 可能会消耗物理核心上的过多执行资源,使共享该物理核心的另一个线程饿死。
您的天真 "fake spinlock" 可能会执行无关的写入操作,从而导致不良的缓存行为。当您对 x86/x86_64 CPU 执行读取-修改-写入操作时(就像 try_lock 可能执行的 compare/exchange 一样),它总是写入,即使值不是变了。此写入导致缓存行在其他内核上无效,要求它们在另一个内核访问该行时重新共享它。如果其他内核上的线程同时争用同一个锁,那就太糟糕了。
你的天真 "fake spinlock" 与分支预测的交互很糟糕。当你最终获得锁时,你会在你锁定其他线程并需要尽快执行的地方获取所有错误预测分支的母体。这就像一个 运行 运动员在起跑线上打起精神准备 运行 但当他听到发令枪声时,他停下来喘口气。
基本上,该代码做错了自旋锁可能做错的所有事情。绝对没有什么是有效地完成的。编写良好的同步原语需要深厚的硬件专业知识。
使用自旋锁的主要好处是,如果最重要的先决条件为真,则获取和释放的成本非常低:锁上很少或没有拥塞 .
如果您有足够的把握知道不会存在争用,自旋锁将大大优于互斥锁的简单实现,互斥锁将通过库代码执行您不一定需要的验证,并执行系统调用。这意味着进行上下文切换(消耗数百个周期),并放弃线程的时间片并导致您的线程被重新安排。这可能需要一段不确定的时间——即使锁定几乎立即可用,在不利条件下您的线程再次 运行s 之前您仍然需要等待几十毫秒。
但是,如果无争用的前提条件不成立,自旋锁通常会差很多,因为它没有进展,但它仍然消耗 CPU 资源,就像它正在执行工作一样。在互斥量上阻塞时,您的线程不会消耗 CPU 资源,因此这些资源可用于不同的线程来完成工作,或者 CPU 可能会降低速度,从而节省电量。使用自旋锁是不可能的,自旋锁一直在执行 "active work" 直到成功(或失败)。
在最坏的情况下,如果等待者的数量大于 CPU 个内核的数量,自旋锁可能会导致 巨大的 ,不成比例的性能影响,因为活跃的线程和 运行ning 在等待 运行ning 时永远不会发生的情况(因为释放锁需要与 运行 不同的线程!)。
另一方面,人们应该期望 std::mutex
的每个现代 no-suck 实现在回退到进行系统调用之前已经包含一个小的自旋锁。但是......虽然这是一个合理的假设,但并不能保证这一点。
使用自旋锁支持 std::mutex
的另一个非技术原因可能是许可条款。许可条款对于设计决策来说是一个糟糕的理由,但它们可能仍然是非常真实的。
例如,目前的 GCC 实现完全基于 pthreads,这意味着 "anything MinGW" 使用标准线程库中的任何东西都必须与 winpthreads 链接(缺少替代品)。这意味着您受 winpthreads 许可的约束,这意味着您必须复制他们的版权信息。对于某些人来说,这是一个交易破坏者。
使用专门设计的自旋锁(例如 http://anki3d.org/spinlock)与这样的代码相比有什么好处:
std::mutex m;
while (!m.try_lock()) {}
# do work
m.unlock();
在典型的硬件上,有很多好处:
您的天真 "fake spinlock" 可能会在 CPU 自旋时使内部 CPU 总线饱和,从而使其他物理内核(包括持有锁的物理内核)处于饥饿状态。
如果 CPU 支持超线程或类似的东西,你的天真 "fake spinlock" 可能会消耗物理核心上的过多执行资源,使共享该物理核心的另一个线程饿死。
您的天真 "fake spinlock" 可能会执行无关的写入操作,从而导致不良的缓存行为。当您对 x86/x86_64 CPU 执行读取-修改-写入操作时(就像 try_lock 可能执行的 compare/exchange 一样),它总是写入,即使值不是变了。此写入导致缓存行在其他内核上无效,要求它们在另一个内核访问该行时重新共享它。如果其他内核上的线程同时争用同一个锁,那就太糟糕了。
你的天真 "fake spinlock" 与分支预测的交互很糟糕。当你最终获得锁时,你会在你锁定其他线程并需要尽快执行的地方获取所有错误预测分支的母体。这就像一个 运行 运动员在起跑线上打起精神准备 运行 但当他听到发令枪声时,他停下来喘口气。
基本上,该代码做错了自旋锁可能做错的所有事情。绝对没有什么是有效地完成的。编写良好的同步原语需要深厚的硬件专业知识。
使用自旋锁的主要好处是,如果最重要的先决条件为真,则获取和释放的成本非常低:锁上很少或没有拥塞 .
如果您有足够的把握知道不会存在争用,自旋锁将大大优于互斥锁的简单实现,互斥锁将通过库代码执行您不一定需要的验证,并执行系统调用。这意味着进行上下文切换(消耗数百个周期),并放弃线程的时间片并导致您的线程被重新安排。这可能需要一段不确定的时间——即使锁定几乎立即可用,在不利条件下您的线程再次 运行s 之前您仍然需要等待几十毫秒。
但是,如果无争用的前提条件不成立,自旋锁通常会差很多,因为它没有进展,但它仍然消耗 CPU 资源,就像它正在执行工作一样。在互斥量上阻塞时,您的线程不会消耗 CPU 资源,因此这些资源可用于不同的线程来完成工作,或者 CPU 可能会降低速度,从而节省电量。使用自旋锁是不可能的,自旋锁一直在执行 "active work" 直到成功(或失败)。
在最坏的情况下,如果等待者的数量大于 CPU 个内核的数量,自旋锁可能会导致 巨大的 ,不成比例的性能影响,因为活跃的线程和 运行ning 在等待 运行ning 时永远不会发生的情况(因为释放锁需要与 运行 不同的线程!)。
另一方面,人们应该期望 std::mutex
的每个现代 no-suck 实现在回退到进行系统调用之前已经包含一个小的自旋锁。但是......虽然这是一个合理的假设,但并不能保证这一点。
使用自旋锁支持 std::mutex
的另一个非技术原因可能是许可条款。许可条款对于设计决策来说是一个糟糕的理由,但它们可能仍然是非常真实的。
例如,目前的 GCC 实现完全基于 pthreads,这意味着 "anything MinGW" 使用标准线程库中的任何东西都必须与 winpthreads 链接(缺少替代品)。这意味着您受 winpthreads 许可的约束,这意味着您必须复制他们的版权信息。对于某些人来说,这是一个交易破坏者。