2

https://www.justsoftwaresolutions.co.uk/threading/petersons_lock_with_C++0x_atomics.html 我写了评论,问了两个问题,对安东尼的回复还有另一个问题。
这是回复:

"1. flag0和flag1变量上的acquire/release是必要的,以确保它充当锁:unlock中的release store与next lock中的acquire-load同步,以确保在锁的同时修改数据被持有的现在对第二个线程可见。”

我用 C 写了一个彼得森锁

typedef struct {
  volatile bool flag[2];
   volatile int victim;
  } peterson_lock_t;

  void peterson_lock_init(peterson_lock_t &lock) {
   lock.flag[0] = lock.flag[1] = false;
   lock.victim = 0;
  } 

  void peterson_lock(peterson_lock_t &lock, int id) {
   lock.flag[id] = true;
   lock.victim = id;
   asm volatile ("mfence" : : : "memory");
   while (lock.flag[1 - id] && lock.victim == id) {
   };
  }

  void peterson_unlock(peterson_lock_t &lock, int id) {
   lock.flag[id] = false;
  }

我测试了它,我认为它是正确的,对吧?

如果正确,我的问题是我是否需要添加 sfence 和 lfence 以“确保在持有锁时修改的数据现在对第二个线程可见”?像这样,

  void peterson_lock(peterson_lock_t &lock, int id) {
   lock.flag[id] = true;
   lock.victim = id;
   asm volatile ("mfence" : : : "memory");
   asm volatile ("lfence" : : : "memory"); // here, I think this is unnecessary, since mfence will flush load buffer
   while (lock.flag[1 - id] && lock.victim == id) {
   };
  }

  void peterson_unlock(peterson_lock_t &lock, int id) {
   asm volatile ("sfence" : : : "memory"); // here
   lock.flag[id] = false;
  }

我认为没有必要这样做。我的理解是,在 x86/64 上,'store' 有一个释放语义,'load' 有一个获取语义(根本原因是在 x86/64 上只有 store 加载重新排序),并且 'lock.flag[id]= false' 是一个'store','lock.flag[1 - id]' 是一个'load',所以在 Dmitriy 的实现中不需要对 flag0 和 flag1 做获取/释放之类的事情

编辑@Anthony 非常感谢您的重播。是的,我需要避免编译器重新排序。那么,像下面这样的修改,是否正确?因为对于 x86,只需要在 'peterson_unlock' 中禁止编译器重新排序

void peterson_lock(peterson_lock_t &lock, int id) {
    lock.flag[id] = true;
    lock.victim = id;
    asm volatile ("mfence" : : : "memory");
    while (lock.flag[1 - id] && lock.victim == id) {
    };
}

void peterson_unlock(peterson_lock_t &lock, int id) {
    asm volatile ("" : : : "memory"); // here, forbidden compiler reorder
    lock.flag[id] = false;
}
4

1 回答 1

2

原子操作及其内存排序标志的使用不仅仅是选择指令。它还会影响编译器的优化器。

volatile读写不能相互重新排序,必须发出,但可以与其他代码自由重新排序。

在没有同步的情况下从多个线程访问volatile非原子变量是未定义的行为,就像对volatile非非原子变量一样。

因此

int a;
peterson_lock(some_lock,0);
a=42;
peterson_unlock(some_lock,0);

可能会重新排序

int a;
a=42;
peterson_lock(some_lock,0);
peterson_unlock(some_lock,0);

或者

int a;
peterson_lock(some_lock,0);
peterson_unlock(some_lock,0);
a=42;

两者都没有保留锁定功能。

因为具有memory_order_release排序的存储确保先前的写入对具有memory_order_acquire排序的稍后加载可见,所以这实质上意味着编译器无法在解锁之后重新排序先前的存储,如果您将原子操作与memory_order_release.

同样,因为带有memory_order_acquire排序的加载确保来自另一个线程的写入现在可能在以前不可见时可见,所以这实质上意味着编译器无法在锁定之前重新排序后续加载,如果您使用带有memory_order_acquire.

简而言之:您需要锁定和解锁中的内存排序约束,不仅是为了选择指令,而且(同样重要的是)对编译器的影响。

对于 x86,relaxed、acquire 和 seq_cst 加载都是不受保护的mov指令,但它们对编译器的影响却大不相同。

如果您不需要原子操作的内存排序语义,请memory_order_relaxed在所有操作上使用。这将确保操作的原子性(并避免未定义的行为),而不会添加无关的排序要求。因此,每当您想使用volatile变量进行同步时,您都应该使用atomic具有适当内存顺序(包括memory_order_relaxed)的变量。

永远不需要在 C++ 代码中添加额外asm的同步语句。原子操作和栅栏功能就足够了。

您的代码仍然不正确,因为flag变量和victim变量都被多个线程触及,并且不是原子的。

于 2017-04-18T07:41:53.243 回答