атомарность и модели памяти c++

Читаю "C++ практика многопоточного программирования" Уильямса Э и не могу понять почему в предложенном коде, в компонентной функции test_and_set достаточно указать memory_order_aqcuire для корректной работы мьютекса.На мой взгляд мы можем так же зайти в защищенную мьютексом область кода(в которой уже работает некий поток) из другого потока, ведь после записи в флаг значения тру(захвате блокировки) нет гарантии, что другой поток их моментально увидит. И за одно хотелось, чтобы посоветовали хорошую литературу или лекции по данной подтеме, потому что по ощущениям в читаемой мною книге не очень хорошо объяснена конкретная тема

class spinlock_mutex
    {
        std::atomic_flag flag;
    public:
        spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
        
        void lock()
        {
            while(flag.test_and_set(std::memory_order_acquire));
        }
        
        void unlock()
        {
            flag.clear(std::memory_order_release);
        }
    };

Ответы (2 шт):

Автор решения: HolyBlackCat

У каждой атомарной переменной есть свой так называемый modification order (порядок изменения?). Это последовательность значений, которые та принимает во время работы программы.

Все потоки всегда наблюдают один и тот же порядок (для каждой отдельной атомарной переменной), при любых std::memory_order-ах.

(Понятно, что этот список значений нигде не хранится, это только абстракция из стандарта.)


нет гарантии, что другой поток их моментально увидит

Тут спасает порядок модификации.

Я так понимаю, вы представляете себе пример в духе:

  1. Поток 1 делает flag.test_and_set(...) и получает ноль.
  2. Поток 1 меняет какую-то не-атомарную переменную (допустим int x; x = 10), но еще не успевает сбросить флаг.
  3. Поток 2 делает flag.test_and_set(...) и получает ноль.
  4. Поток 2 меняет ту же самую не-атомарную переменную (x = 20). Есть конфликт с шагом (2) или нет?

Так вот, у flag есть порядок модификации. Он не может быть такой:

Поток 1 читает 0 и пишет 1
Поток 2 читает 0 и пишет 1
(и дальше оба потока пишут ноль, не важно в каком порядке)

Потому что это противоречие. Как второй шаг может прочитать 0, если первый шаг уже записал 1?

Поэтому порядок обязан быть такой:

Поток 1 читает 0 и пишет 1      (A)
Поток 1 пишет 0                 (B)
Поток 2 читает 0 и пишет 1      (C)
Поток 2 пишет 0                 (D)

(Или второй поток перед первым, неважно.)


Но это все равно неполное доказательство. Какое нам дело до порядка модификации флага? Как он мешает компилятору выдвинуть работу с x из под спинлок и все равно получить гонку?

Для этого нужны memory_order_acquire и release (seq_cst тоже подходит, потому что он еще строже, а relaxed не подходит).

Когда второй поток читает флаг (в режиме acquire) и видит 0 (шаг (C)), этот ноль к нему пришел из .clear() в первом потоке (шаг (B), в режиме release).

Тогда говорят, что release-запись "синхронизируется" c acquire-чтениями той же самой переменной, идущими прямо за записью (т.е. если между ними в порядке модификации нет других записей).

Синхронизация гарантирует, что все, что происходило до (B) (т.е. наш x = 10), произойдет также и до (C), а x = 20 происходит после (C). Поэтому x = 10 гарантированно идет перед x = 20, и гонки нет.


посоветовали хорошую литературу или лекции по данной подтеме

Я слышал хорошие вещи про Concurrency in Action.

Еще, я когда несколько лет назал сел разбираться с атомиками, описал свое понимание тут: What do each memory_order mean?. Может будет полезно.

→ Ссылка
Автор решения: Serge3leo

почему в предложенном коде, в компонентной функции test_and_set достаточно указать memory_order_aqcuire для корректной работы мьютекса.

Грубо говоря, потому-что в функции flag.clear используется memory_order_release (к примеру, если бы там было - memory_order_relaxed, то была бы некорректная работа, данные иногда бы "не записывались" или "не счтывались").

Как обычно определяется мьютекс:

  1. Эксклюзивное исполнение, только один поток владеет мьютексом;
  2. Поток, который захватил мьютекс, имеет доступ к той информации, которую создал предыдущий владелец (или она ему была доступна);
  3. Информация, которую создал текущий владелец до освобождения мьютекса, будет доступна следующему владельцу.

В модели памяти C/C++ это выражено отношениями:

  • Между видимой записью некоей переменной в потоке X и атомарной переменной flag - std::memory_order_release;

  • Семантикой атомарных операций flag.clear() и flag.test_and_set();

  • Между атомарной переменной flag и видимым чтением этой же переменной в потоке Y - std::memory_order_acquire

На мой взгляд мы можем так же зайти в защищенную мьютексом область кода(в которой уже работает некий поток) из другого потока, ведь после записи в флаг значения тру(захвате блокировки) нет гарантии

Не очень понятно, с одной стороны, да, если другой поток не захватывает тот же самый экземпляр переменной flag, то получается состояние гонки, ввиду того, что неизвестно, ни когда "запись" в "завершится", ни когда "чтение" реально "произойдёт".

другой поток их моментально увидит.

С другой стороны, если один поток освобождает flag, а другой захватывает flag, то все отношения выполняются. И для выполнения этих отношений совершенно не требуется никакой "моментальности", но только по результату процесса освобождения/захвата (бывает, достаточно ресурсоёмкого, зависит от архитектуры и прочего, прочего).

И за одно хотелось, чтобы посоветовали хорошую литературу или лекции по данной подтеме, потому что по ощущениям в читаемой мною книге не очень хорошо объяснена конкретная тема

В деле модели памяти, если в этих книгах написано то, что в стандарте, то зачем они, когда у нас есть стандарт? Если же в этих книгах написано не то, что в стандарте, то тем более их ... (по крайней мере, в этом месте, читать не стоит).

На мой непросвещённый взгляд, в стандартах C/C++ модель памяти изложена максимально просто. А всякие упрощённые изложения во всяких "мурзилках", нарушают стандарт.

Если цель разработка алгоритмов синхронизации, то [intro.multithread] и [atomics.order]. Но, если просто для некоторого понимания, при использовании готовых шаблонов, то и Уильямс неплох, достаточно близко к стандарту.

→ Ссылка