Никто не использует блокировку Петерсона на основных платформах, потому что мьютексы доступны.Но предполагая, что вы не можете использовать их, и вы пишете код для старой платформы X86
без доступа к современным примитивам (без модели памяти, без мьютексов, без атомарных операций RMW), этот алгоритм может быть рассмотрен.
Ваша реализация блокировки Петерсона неверна (также после замены строк «Пометить как A» и «Пометить как B»).
Если вы переводите псевдокод Википедии в 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
является операцией освобождения на уровне ЦП, компилятор все еще может переупорядочивать операции с volatile
и не- volatile
данными.
По этой причинеЯ добавил барьер компилятора перед сбросом lock.flag[id]
в peterson_unlock
.
Но, вероятно, будет хорошей идеей использовать volatile
для всех данных, которые разделяются между потоками с использованием этого алгоритма, потому что компилятор можетпо-прежнему выполнять операции сохранения и загрузки не только данных volatile
только в регистре ЦП.
Обратите внимание, что при использовании volatile
для общих данных барьер компилятора в peterson_unlock
становится избыточным.