关于使用mfence / lfence / sfence对第二个线程可见的锁定中的数据修改的peterson算法

时间:2017-04-17 03:21:39

标签: memory memory-fences memory-barriers

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

" 1。 flag0和flag1变量的获取/释放是必要的,以确保它充当锁:解锁中的释放存储与下一个锁中的获取 - 加载同步,以确保在锁被保持时修改的数据是现在可见第二个帖子。"

我在C

写了一个peterson锁
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'有一个发布语义,并且“加载”#39;有一个获取语义(根本原因在x86 / 64上只有商店加载重新排序), 和' lock.flag [id] = false'是一个'商店' lock.flag [1-id]'是一个负载', 所以没有必要在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;
}

1 个答案:

答案 0 :(得分:2)

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

volatile读取和写入不能相互重新排序,必须发布,但可以与其他代码一起自由重新排序。

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

因此

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排序是可见的,这实质上意味着编译器无法在解锁之前重新排序先前的存储, if您使用memory_order_release 的原子操作。

同样,因为具有memory_order_acquire排序的加载确保来自另一个线程的写入现在可以在以前不可见时显示,这实质上意味着编译器无法在锁定之前重新排序后续加载, if您使用memory_order_acquire 的原子操作。

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

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

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

您应该从不需要在C ++代码中添加额外的asm语句以进行同步。原子操作和围栏功能就足够了。

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