如何在 std::atomic<T> 上实现一个简单的自旋锁,以便编译器不会对其进行优化?

Posted

技术标签:

【中文标题】如何在 std::atomic<T> 上实现一个简单的自旋锁,以便编译器不会对其进行优化?【英文标题】:How do I implement a simple spinlock on std::atomic<T> so that the compiler doesn't optimize it out? 【发布时间】:2016-03-23 01:04:59 【问题描述】:

我一直在尝试使用标准 C++11/14 锁定线程的非常简单的方法,目前我最终使用了类似这样的东西

std::atomic<bool> setup_ready(false);


  // thread 1
  while (!setup_ready.load()) std::this_thread::yield();

  // do something



  // thread 2
  // perform some setup
  setup_ready.store(true);

是否有什么东西阻止编译器优化this_thread::yield() 调用,进而优化整个循环?

如果我完全删除了 yield 并只想像这样忙着等待怎么办?是否有跨平台的方法来防止编译器优化循环?还是标准在循环原子时阻止它?


  // thread 1
  while (!setup_ready.load()) ;

  // do something

我想出的唯一解决方案是在混合中添加一个 volatile 变量,但我不确定这是否是最好的方法。我也尝试检查有关原子的标准,但我找不到很多关于编译器优化细节的细节。

【问题讨论】:

标准没有明确允许大多数编译器优化;它们在很大程度上取决于实现。如果您删除了产量,我非常怀疑任何编译器都会优化您的循环。我知道 gcc 和 cl 肯定不会。 编译器知道原子可以“自发地”改变,这要归功于其他线程中的写入,因此它无法优化循环。 不确定你所说的“反过来整个循环”是什么意思 - 循环包含 2 个调用(setup_ready.load()yield),即使其中 1 个调用被优化了它也没有暗示另一个会是。 顺便说一句:(我确实曾经看到过一个关于此的问题,但现在找不到)使用yield 实现自旋锁被认为是不好的。理由是yield 最终会消失很长时间。但是自旋锁应该只在锁内的代码非常快(当然不会进行任何系统调用)的情况下首先使用。 IOW,如果没有yield 的自旋会影响性能,那么这首先不是一个拥有自旋锁的好地方,最好等待同步对象。 @M.M 实际上我的第一个实现不包括产量,但通过添加它,我能够获得 10 倍的性能提升。最后我最终还是切换到了一个条件变量,但是是的,我明白你的意思了。 【参考方案1】:

不允许编译器优化this_thread::yield(),除非它可以确定它没有副作用——它不能,因为它不是。

原子加载操作无法被优化掉,因为它包含一个内存屏障,并且专门定义为获取其他线程所做的修改。

【讨论】:

谢谢你,虽然从我读过的内容来看,this_thread::yield() 是实现定义的,并且由于它主要是一个提示,它可能是一个 NOP,这反过来又没有副作用并且可以优化。至少这是我从阅读标准中得到的。 如果它是一个 NOP,那么它是否被优化并不重要。它仍然需要运行循环来执行原子加载 函数是 NOP 并不意味着编译器可以优化掉它。仅当函数定义对编译器可见时才有可能,这通常意味着它是内联的,或者您正在进行过程间优化(并且您的 C 或 C++ 运行时不太可能提供启用 IPO 所需的信息)。 @JonathanWakely 因此,如果我调用了一个完全为空的非内联函数,大多数编译器不会完全删除该调用?还是我误解了你的意思? 这就是我要说的。如果编译器不知道函数的作用,它如何删除对函数的调用? (不一定必须是内联的,但函数体必须在正在编译的翻译单元中可见,这仅适用于在同一翻译单元中定义的函数,无论是否内联)。

以上是关于如何在 std::atomic<T> 上实现一个简单的自旋锁,以便编译器不会对其进行优化?的主要内容,如果未能解决你的问题,请参考以下文章

在 C++11 中以无锁方式原子交换两个 std::atomic<T*> 对象?

为啥 std::atomic<T>::is_lock_free() 不像 constexpr 一样是静态的?

C++ 原子操作 std::atomic<int>

队列<T>上的原子操作?

在 Windows 中等待 std::atomic<int> 的正确方法?

std::atomic<std::string> 是不是正常工作?