为什么带有 std::memory_order_relaxed 的自旋锁可以正确执行?

why does spinlock with std::memory_order_relaxed perform correctly?

我用 C++11 原子库实现了自旋锁:

class SpinLock {
  atomic_bool latch_;

  public:
  SpinLock() :latch_(false){
  }
  void lock() {
    while(tryLock() == false);
  }
  bool tryLock() {
    bool b = false;
    return latch_.compare_exchange_weak(b,true,std::memory_order_relaxed);
  }
  void unlock() {
    latch_.store(false,std::memory_order_relaxed);
  }
};

我通过生成多个线程来测试正确性,如下所示:

static int z = 0;
static SpinLock spinLock;
static void safeIncrement(int run) {
  while(--run >= 0) {
    std::lock_guard<SpinLock> guard(spinLock);
    ++z;
  }
}

static void test(int nThreads =2) {
  std::vector<std::thread*> workers(nThreads);
  z = 0;
  for(auto& ptr : workers) ptr = new std::thread(safeIncrement,1<<20);
  for(auto ptr : workers) ptr->join();
  cout<<"after increment: " <<z << " out of " << (1<<20) * nThreads<<endl;
  for(auto ptr : workers) delete ptr;
}
int main() {

  test(4);

  return 0;

}

我很惊讶最后的总数加起来是一个正确的值,而且顺序很宽松。通过这篇文章:http://en.cppreference.com/w/cpp/atomic/memory_order,宽松的顺序意味着 "there are no synchronization or ordering constraints",所以一个线程的更改并不意味着其他线程可以看到,对吗?为什么还是正确的?
(测试是在 Intel(R) Core(TM) i5-3337U CPU @ 1.80GHz 上 运行)

编辑:(感谢 Maxim 的评论)更新代码:初始化 SpinLock 中的数据成员,并更新测试代码。

我看到至少 x86-64 的 GCC 6.3 为 relaxed 和 release/acquire 生成了相同的代码。因此,结果相同也就不足为奇了。因此,要查看差异,您可能需要比 x86-64 提供的 TSO 更宽松的内存架构。大概,可能是ARM。

对互斥量实施使用宽松的排序约束会导致灾难。
根据定义,互斥锁旨在同步线程之间的数据。术语 acquirerelease 与互斥锁密切相关; 您 获取 一个互斥锁,更改受其保护的数据并 释放 互斥锁,以便一旦另一个线程获取相同的互斥锁,数据就对它可见。

您引用的文章指出,对于宽松的操作,“没有同步或排序约束”...它适用于互斥体周围的内存操作, 不是互斥锁本身。通过宽松的排序,本应受互斥锁保护的数据实际上可能会被多个线程同时修改(引入数据竞争)。

在更强大的有序架构上,例如 X86,它具有隐含的 acquire/release 语义,您将摆脱这种实现(因此您的测试成功)。 但是,运行 它在使用较弱内存排序的体系结构上,例如 PowerARMv7,你就有麻烦了。

您在评论中建议的顺序是正确的。

C++11 标准为原子操作指定了最弱的保证。并非所有硬件都能准确匹配每个最弱的保证,因此编译器和库编写者有时必须 "round up" 进行更强的操作。例如,x86 上的所有原子 read-modify-write 操作隐式具有 memory_order_acq_rel.

另外,硬件架构的具体实现可能比硬件手册上说的更有保障。例如,早期的 Itaniums 实现了 memory_order_acq_rel 语义,甚至对于一些只承诺 memory_order_release.

的硬件指令也是如此

理论上,您的代码有可能在 x86 上失败,因为尊重原子操作的内存顺序涉及硬件和编译器。激进的编译器可以合法地将 'z' 的负载(也可能是存储!)移到仅使用 memory_order_release 排序的 tryLock 操作上。