Существует популярная версия мьютекса спин-блокировки, которая распространяется по Интернету и которую можно встретить в книге Энтони Уильямса (С++ Concurrency в действии). Вот он:
class SpinLock
{
std::atomic_flag locked;
public:
SpinLock() :
locked{ATOMIC_FLAG_INIT}
{
}
void lock()
{
while(locked.test_and_set(std::memory_order_acquire));
}
void unlock()
{
locked.clear(std::memory_order_release);
}
};
Я не понимаю, почему все используют std::memory_order_acquire
для test_and_set
, который является RMW-операцией. Почему это не std::memory_acq_rel
?
Предположим, что у нас есть 2 потока одновременно, пытаясь получить блокировку:
T1: test_and_set -> ret false
T2: test_and_set -> ret false
Ситуация должна быть возможной, так как мы имеем 2 acquire
операции, которые не образуют никакого отношения sync with
между собой. Да, после того, как мы разблокировали мьютекс, мы имеем операцию release
, которая гласит следующий release sequence
, и жизнь становится красочной, и все счастливы. Но почему это безопасно, прежде чем заголовок release sequence
?
Так как многие люди упоминают именно эту реализацию, я полагаю, что она должна работать правильно. Так что мне не хватает?
ОБНОВЛЕНИЕ 1:
Я прекрасно понимаю, что операция атомарна, что операции между lock
и unlock
не могут выйти из критического раздела. Это не проблема. Проблема в том, что я не вижу, как вышеприведенный код предотвращает одновременное попадание двух мьютексов в критический раздел. Чтобы этого не произошло, должно произойти до отношения между 2 lock
s. Может ли кто-нибудь показать мне, используя стандартные понятия С++, что код совершенно безопасен?
ОБНОВЛЕНИЕ 2:
Хорошо, мы близки к правильному ответу, я полагаю. Я нашел следующее в стандарте:
[atomics.order] статья 11
Операции Atomic read-modify-write всегда должны считывать последнее значение (в порядке модификации), написанном до записи, связанной с операция чтения-изменения-записи.
И по этой важной ноте я мог бы с радостью закрыть вопрос, но у меня все еще есть свои сомнения. Что относительно части in the modification order
?
Стандарт довольно ясен:
[intro.multithread] пункт 8
Все модификации конкретного атомного объекта M встречаются в некоторых конкретный общий порядок, называемый порядком модификации M. Если A и B - модификации атомного объекта M и A происходит до (как определено ниже) B, то A предшествует B в порядке модификации M, который определен ниже.
Таким образом, согласно этой статье, чтобы операция RMW имела самое последнее письменное значение, последняя операция записи должна произойти до чтения или операции RMW. Это не так в вопросе. Правильно?
ОБНОВЛЕНИЕ 3:
Я все больше думаю, что код для блокировки спина сломан. Вот мои рассуждения. С++ задает 3 типа операций:
- Приобретение, выпуск, получение-релиз - это синхронизация.
- Relaxed - это не sync ops
- RMW - это операции со специальной особенностью
Давайте начнем с RMW и узнаем, что в них такого особенного. Во-первых, они представляют собой ценный актив в формировании release sequence
, во-вторых, у них есть специальное предложение ([atomics.order], пункт 11), процитированное выше. Ничего особенного, что я нашел.
Приобретение/освобождение - это sync ops и release sync with acquire
, образуя связь happens before
. Расслабленные операции - это просто атомы и вообще не участвуют в порядке модификации.
Что мы имеем в нашем коде? У нас есть операция RMW, которая использует семантику памяти для хранения, поэтому всякий раз, когда достигается первая разблокировка (выпуск), она выполняет 2 роли:
- Он формирует связь
sync with
с предыдущимrelease
- Он участвует в
release sequence
. Но все верно только после завершения первогоunlock
.
До этого, если у нас есть 2+ потоки, которые одновременно запускают наш код lock
, тогда мы можем ввести pass lock
одновременно, так как операции 2 acquire
не образуют никакого отношения. Они так же неупорядочены, как и расслабленные операции. Поскольку они неупорядочены, мы не можем использовать какие-либо специальные предложения о работе RMW, так как нет отношения happens before
и, следовательно, никакого порядка модификации для флага locked
.
Таким образом, либо моя логика ошибочна, либо код сломан. Пожалуйста, кто знает правду - прокомментируйте это.