Peterson Lock算法的测试实现?

时间:2012-07-21 00:29:17

标签: c concurrency locking c99

有人知道C中Peterson's Lock algorithm的良好/正确实现吗?我似乎无法找到这个。感谢。

2 个答案:

答案 0 :(得分:6)

彼得森的算法无法在C99中正确实现,如who ordered memory fences on an x86中所述。

彼得森的算法如下:

LOCK:

interested[id] = 1                interested[other] = 1
turn = other                      turn = id

while turn == other               while turn == id
  and interested[other] == 1        and interested[id] == 1


UNLOCK:

interested[id] = 0                interested[other] = 0

这里有一些隐藏的假设。首先,每个线程必须注意它在获得锁定之前获得锁定的兴趣。放弃转弯必须使我们有兴趣获得锁定的其他线程可见。

此外,与每次锁定一样,临界区中的内存访问不能通过lock()调用提升,也不能通过unlock()沉没。即:lock()必须至少具有获取语义,而unlock()必须至少具有释放语义。

在C11中,实现此目的的最简单方法是使用顺序一致的内存顺序,这使得代码运行就像是按程序顺序运行的线程的简单交错(警告:完全未经测试的代码< / strong>,但它类似于Dmitriy V'jukov的Relacy Race Detector)中的一个例子:

lock(int id)
{
    atomic_store(&interested[id], 1);
    atomic_store(&turn, 1 - id);

    while (atomic_load(&turn) == 1 - id
           && atomic_load(&interested[1 - id]) == 1);
}

unlock(int id)
{
    atomic_store(&interested[id], 0);
}

这可确保编译器不会进行破坏算法的优化(通过在原子操作中提升/下载加载/存储),并发出适当的CPU指令以确保CPU也不会破坏算法。没有明确选择内存模型的C11 / C ++ 11原子操作的默认内存模型是顺序一致的内存模型。

C11 / C ++ 11还支持较弱的内存模型,允许尽可能多的优化。以下是由Dmitriy V'jukov在其自己的Relacy Race Detector的语法中由Anthony Williams算法翻译成C11的C11的翻译。 [petersons_lock_with_C++0x_atomics] [the-inscrutable-c-memory-model]。如果这个算法不正确,那就是我的错(警告:也是未经测试的代码,但基于Dmitriy V'jukov和Anthony Williams的优秀代码):

lock(int id)
{
    atomic_store_explicit(&interested[id], 1, memory_order_relaxed);
    atomic_exchange_explicit(&turn, 1 - id, memory_order_acq_rel);

    while (atomic_load_explicit(&interested[1 - id], memory_order_acquire) == 1
           && atomic_load_explicit(&turn, memory_order_relaxed) == 1 - id);
}

unlock(int id)
{
    atomic_store_explicit(&interested[id], 0, memory_order_release);
}

注意与获取和释放语义的交换。交换是一种 原子RMW操作。原子RMW操作始终读取存储的最后一个值 在RMW操作中写入之前。此外,对原子对象的获取 它从同一个原子对象(或任何更晚的版本)上的发行版读取写入 从执行发布的线程或之后的任何线程写入该对象 从任何原子RMW操作写入)创建同步关系 在释放和获得之间。

所以,这个操作是线程之间的同步点,有 始终与一个线程中的交换和...之间的同步关系 任何线程执行的最后一次交换(或者转换的初始化,for 第一次交流)。

因此我们在商店与interested[id]之间建立了一个有序的关系 以及来自/到turn的交换,两者之间的同步关系 来自/到turn的连续交换,以及之前的顺序关系 来自/到turn的交换与interested[1 - id]的加载之间。这个 相当于访问interested[x]之间发生的关系 不同的线程,turn提供线程之间的同步。 这会强制使算法运行所需的所有排序。

那么在C11之前这些事情是如何完成的?它涉及使用编译器和 CPU特有的魔力。作为一个例子,让我们看看非常强烈有序的x86。 IIRC,所有x86加载都具有获取语义,并且所有商店都已发布 语义(保存在SSE中的非时间移动,精确地用于实现更高的 以偶然需要发出CPU围栏来实现的性能为代价 CPU之间的一致性)。但对于彼得森的算法来说,这还不够 Bartosz Milewsky解释说 who-ordered-memory-fences-on-an-x86, 为了使Peterson的算法工作,我们需要在它们之间建立一个排序 访问turninterested,如果不这样做,可能会导致看到负载 在写入interested[1 - id]之前从interested[id]开始,这是一件坏事。

所以在GCC / x86中这样做的方法是(警告:虽然我测试了类似下面的内容,实际上是wrong-implementation-of-petersons-algorithm代码的修改版本,测试远不能确保多线程代码的正确性):

lock(int id)
{
    interested[id] = 1;
    turn = 1 - id;
    __asm__ __volatile__("mfence");

    do {
        __asm__ __volatile__("":::"memory");
    } while (turn == 1 - id
           && interested[1 - id] == 1);
}

unlock(int id)
{
   interested[id] = 0;
}

MFENCE阻止存储和加载到不同的内存地址 重新排序。否则,对interested[id]的写入可以在商店中排队 interested[1 - id]的负载继续进行缓冲。在许多当前 微体系结构SFENCE可能就足够了,因为它可以实现为 存储缓冲区消耗,但IIUC SFENCE不需要以这种方式实现, 并且可以简单地防止商店之间的重新排序。因此SFENCE在任何地方都可能不够,我们需要一个完整的MFENCE

编译器屏障(__asm__ __volatile__("":::"memory"))阻止 编译器决定它已经知道turn的值。我们 告诉编译器我们已经破坏了内存,因此缓存了所有值 必须从内存重新加载寄存器。

P.S:我觉得这需要一个结束的段落,但我的大脑已经耗尽了。

答案 1 :(得分:5)

我不会对实现有多好或正确的做出任何断言,但它已经过测试(简要)。这是维基百科上描述的算法的直接翻译。

struct petersonslock_t {
    volatile unsigned flag[2];
    volatile unsigned turn;
};
typedef struct petersonslock_t petersonslock_t;

petersonslock_t petersonslock () {
    petersonslock_t l = { { 0U, 0U }, ~0U };
    return l;
}

void petersonslock_lock (petersonslock_t *l, int p) {
    assert(p == 0 || p == 1);
    l->flag[p] = 1;
    l->turn = !p;
    while (l->flag[!p] && (l->turn == !p)) {}
};

void petersonslock_unlock (petersonslock_t *l, int p) {
    assert(p == 0 || p == 1);
    l->flag[p] = 0;
};

Greg指出,在具有略微宽松的内存一致性的SMP架构(例如x86)上,尽管对同一内存位置的负载是有序的,但是对一个处理器上的不同位置的负载可能看起来与另一个处理器无序

Jens Gustedt和ninjalj建议修改原始算法以使用atomic_flag类型。这意味着设置标志和转弯将使用atomic_flag_test_and_set并清除它们将使用来自C11的atomic_flag_clear。或者,可以在flag的更新之间施加内存屏障。

编辑:我最初尝试通过写入所有状态的相同内存位置来纠正此问题。 ninjalj指出,按位运算将状态操作转换为RMW,而不是原始算法的加载和存储。因此,需要原子位操作。 C11提供了这样的运算符,GCC也提供了内置函数。下面的算法使用GCC内置函数,但是用宏包装,以便可以很容易地将其更改为其他实现。但是,修改上面的原始算法是首选解决方案。

struct petersonslock_t {
    volatile unsigned state;
};
typedef struct petersonslock_t petersonslock_t;

#define ATOMIC_OR(x,v)   __sync_or_and_fetch(&x, v)
#define ATOMIC_AND(x,v)  __sync_and_and_fetch(&x, v)

petersonslock_t petersonslock () {
    petersonslock_t l = { 0x000000U };
    return l;
}

void petersonslock_lock (petersonslock_t *l, int p) {
    assert(p == 0 || p == 1);
    unsigned mask = (p == 0) ? 0xFF0000 : 0x00FF00;
    ATOMIC_OR(l->state, (p == 0) ? 0x000100 : 0x010000);
    (p == 0) ? ATOMIC_OR(l->state, 0x000001) : ATOMIC_AND(l->state, 0xFFFF00);
    while ((l->state & mask) && (l->state & 0x0000FF) == !p) {}
};

void petersonslock_unlock (petersonslock_t *l, int p) {
    assert(p == 0 || p == 1);
    ATOMIC_AND(l->state, (p == 0) ? 0xFF00FF : 0x00FFFF);
};