为什么带有std :: memory_order_relaxed的spinlock正确执行?

时间:2017-02-06 09:04:31

标签: multithreading c++11 atomic memory-barriers spinlock

我使用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,宽松的顺序意味着“没有同步或排序约束”,所以从一个线程的变化并不意味着从其他人看到,对吧?为什么它仍然正确?
(该测试在Intel(R)Core(TM)i5-3337U CPU @ 1.80GHz上运行)

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

3 个答案:

答案 0 :(得分:1)

我看到至少G86 6.3在x86-64上为轻松和发布/获取生成了相同的代码。因此,结果是相同的并不奇怪。因此,要看到差异,您可能需要比x86-64提供的TSO更宽松的内存架构。可能是它可能是ARM。

答案 1 :(得分:1)

C ++ 11标准规定了原子操作最薄弱的保证。并非所有硬件都能完全匹配每个最薄弱的保证,因此编译器和库编写者有时必须&#34;向上舍入&#34;加强运营。例如,x86上的所有原子读取 - 修改 - 写入操作都隐式具有memory_order_acq_rel

此外,硬件架构的特定实现可能比硬件手册所说的更强。例如,早期的Itaniums实现了memory_order_acq_rel语义,即使对于只承诺memory_order_release的一些硬件指令也是如此。

理论上,您的代码可能在x86上失败,因为尊重原子操作的内存排序涉及两者硬件和编译器。积极的编译器可以合法地移动&#39; z&#39; (也可能是商店!)仅仅使用tryLock订购的memory_order_release操作。

答案 2 :(得分:0)

对互斥锁实现使用宽松的排序约束是灾难的一个方法 根据定义,互斥锁用于在线程之间同步数据。术语获取发布与互斥锁密切相关; 您获取互斥锁,更改受其保护的数据并释放互斥锁,以便在获取相同的互斥锁后,数据对另一个线程可见。

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

在一个更强大的有序体系结构(例如X86,具有隐式获取/释放语义)上,您将完成此实现(因此您的测试成功)。 但是,在使用较弱内存排序的架构上运行它,例如PowerARMv7,您就遇到了麻烦。

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