我们需要多少个内存屏障来实现彼得森锁?

时间:2019-06-06 02:42:26

标签: c++ x86 mutex memory-barriers

我试图弄清楚实现一个Peterson锁我们需要多少个内存屏障。显然,我们至少需要一个。

https://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/

实际上,基于在不同体系结构中执行的大量测试,似乎一个就足够了。但是,从理论上讲,我们还需要其他吗?

我尝试了下面的代码

my peterson_lock failed in this situation

在Mark A和Mark B之间更改顺序,这样就可以了!但是,内存隔离栅不能捕获Mark A和Mark B之间的顺序。那么,这是否意味着程序仍然不正确?

#include <pthread.h>

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.victim = id; // Mark as A
    lock.flag[id] = true; // Mark as B
    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;
    lock.victim = id;
}

替换了“标记为A”和“标记为B”两行的顺序后,我希望程序几乎总是能够正确运行,因为它现在与Peterson锁上的Wikipedia条目一致。

https://en.wikipedia.org/wiki/Peterson%27s_algorithm

但是,内存隔离栏不能保护Mark A和Mark B之间的顺序。因此,程序是否仍然有可能不正确?如果是,该如何解决?

1 个答案:

答案 0 :(得分:2)

因为互斥体可用,所以没有人在主流平台上使用Peterson锁。 但是,假设您无法使用它们,并且您正在为旧的X86平台编写代码而又无法访问现代原语(没有内存模型,没有互斥锁,没有原子RMW操作),则可以考虑使用此算法。

您对Peterson锁的实现不正确(也在交换了“标记为A”和“标记为B”行之后)。
如果将Wikipedia伪代码转换为C++,则正确的实现将变为:

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

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

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

除了在volatile变量上使用lock之外,还需要mfence中的peterson_lock指令来防止#StoreLoad重新排序。 这显示了一种罕见的情况,即算法需要顺序一致性。即,对lock变量的操作必须以单个总顺序进行。

volatile的使用基于gcc/X86上不可移植(但几乎正确)的属性。 “'几乎'正确”,因为即使volatile上的X86存储是CPU级别的释放操作,编译器仍可以对volatile和非volatile上的操作重新排序数据。
因此,我在lock.flag[id]中重置peterson_unlock之前添加了一个编译器屏障。

但是使用此算法在线程之间共享的所有数据上使用volatile可能是一个好主意, 因为编译器仍然只能对CPU寄存器中的非volatile数据执行存储和加载操作。

请注意,在共享数据上使用volatile时,peterson_unlock中的编译器屏障将变得多余。