Атомарный релиз что такое
std::atomic. Модель памяти C++ в примерах
Язык C++ компилируется под множество архитектур, поэтому в вопросе синхронизации данных между потоками в С++11 была добавлена модель памяти, которая обобщает механизмы синхронизации различных архитектур, позволяя генерировать для каждого процессора оптимальных код с необходимой степенью синхронизации.
Отсюда следует несколько важных выводов: модель синхронизации памяти C++ — это «искусственные» правила, которые учитывают особенности различных архитектур процессоров. В модели C++ некоторые конструкции, описанные стандартом как undefined behavior (UB), могут корректно работать на одной архитектуре, но приводить к ошибкам работы с памятью на других архитектурах.
Наша задача, как разработчиков на языке C++, состоит в том, чтобы писать корректный с точки зрения стандарта языка код. В этом случае мы можем быть уверены, что для каждой платформы будет сгенерирован корректный машинный код.
Код каждого потока компилируется и выполняется так, как будто он один в программе. Вся синхронизация данных между потоками возложена на плечи атомиков ( std::atomic ), т.к. именно они предоставляют возможность форсировать «передачу» изменений данных в другой поток. Далее я покажу, что мьютексы ( std::mutex ) и другие многопоточные примитивы либо реализованы на атомиках, либо предоставляют гарантии, семантически похожие на атомарные операции. Поэтому ключом к написанию корректных многопоточных программ является понимание того, как конкретно работают атомики.
Три слона
На мой взгляд, основная проблема с атомиками в C++ состоит в том, что они несут сразу три функции. Так на каких же трех слонах держатся атомики?
Атомики позволяют реализовать… атомарные операции.
Атомики накладывают ограничения на порядок выполнения операций с памятью в одном потоке.
Синхронизируют память в двух и более потоках выполнения.
Рассмотрим простой пример read-modify-write операции, а именно прибавление к числу единицы. Пример 0, link:
Про ограничения на порядок выполнения операций. Когда мы пишем код программы, то предполагаем, что операторы языка будут выполнены последовательно. В реальности же компилятор и в особенности процессор могут переупорядочить команды программы с целью оптимизации. Они это делают с учетом ограничений на порядок записи и чтения в локацию памяти. Например, чтение из локации памяти должно происходить после записи, эти операции нельзя переупорядочить. Применение атомарных операция может накладывать дополнительные ограничения на возможные переупорядочивания операций с памятью.
Про синхронизацию данных между потоками. Если мы хотим изменить данные в одном потоке и сделать так, чтобы эти изменения были видны в другом потоке, то нам необходимы примитивы многопоточного программирования. Фундаментальным таким примитивом являются атомики, остальные, например мьютексы, либо реализованы на основе атомиков, либо повторяют семантику атомиков. Все остальные попытки записывать и читать одни и те же данные из разных потоков могут приводить к UB.
Случаи, когда синхронизация памяти не требуется:
Если все потоки, работающие с одним участком памяти, используют ее только на чтение
Если разные потоки используют эксклюзивно разные участки памяти
Неделимый, но расслабленный
модификация переменной «появится» в другом потоке не сразу
поток thread2 «увидит» значения одной и той же переменной в том же порядке, в котором происходили её модификации в потоке thread1
порядок модификаций разных переменных в потоке thread1 не сохранится в потоке thread2
Можно использовать relaxed модификатор в качестве счетчика. Пример 1, link:
Использование в качестве флага остановки. Пример 2, link:
Пример неверного использования relaxed в качестве флага готовности данных. Пример 3, link:
Полный порядок
Флаг синхронизации памяти «единая последовательность» (sequential consistency, seq_cst ) самый строгий и понятный. Его свойства:
порядок модификаций разных атомарных переменных в потоке thread1 сохранится в потоке thread2
все потоки будут видеть один и тот же порядок модификации всех атомарных переменных. Сами модификации могут происходить в разных потоках
Таким образом можно представить seq_cst операции, как барьеры памяти, в которых состояние памяти синхронизируется между всеми потоками программы. Другими словами, как будто многопоточная программа выполняется на одноядерном процессоре.
Этот флаг синхронизации памяти в C++ используется по-умолчанию, т.к. с ним меньше всего проблем с точки зрения корректности выполнения программы. Но seq_cst является дорогой операцией для процессоров, в которых вычислительные ядра слабо связаны между собой в плане механизмов обеспечения консистентности памяти. Например, для x86-64 seq_cst дешевле, чем для ARM архитектур.
Продемонстрируем второе свойство. Пример 4, из книги [1], link:
Синхронизация пары. Acquire/Release
Флаг синхронизации памяти acquire/release является более тонким способом синхронизировать данные между парой потоков. Два ключевых слова: memory_order_acquire и memory_order_release работают только в паре над одним атомарным объектом. Рассмотрим их свойства:
модификация атомарной переменной с release будет мгновенно видна в другом потоке, выполняющим чтение этой же атомарной переменной с acquire
Рассмотрим такой пример, называемый Double Checked Locking Anti-Pattern из [2]. Пример 6, link:
Рассмотрим следующую последовательность действий во времени:
lock мьютекса ( acquire )
unlock мьютекса ( release )
2. далее в игру вступает thread2 :
if(initalized) возвращает true (память, где содержится initialized могла быть неявно синхронизирована между ядрами процессора)
singleton->do_job() приводит к segmentation fault (указатель singleton не обязан был быть синхронизирован с потоком thread1 )
Этот случай интересен тем, что наглядно показывает роль мьютекса не только как примитива синхронизации потока выполнения, но и синхронизации памяти.
Семантика acquire/release классов стандартной библиотеки
Механизм acquire/release поможет понять гарантии синхронизации памяти, которые предоставляют классы стандартной библиотеки для работы с потоками. Ниже приведу список наиболее часто используемых операций.
std::thread::(constructor) vs функция потока
Вызов конструктора объекта std::thread ( release ) синхронизирован со стартом работы функции нового потока ( acquire ). Таким образом функция потока может видеть все изменения памяти, которые произошли до вызова конструктора в исходном потоке.
std::thread::join vs владеющий поток
После успешного вызова join поток, в котором был вызван join, «увидит» все изменения памяти, которые были выполнены завершившимся потоком.
std::mutex::lock vs std::mutex::unlock
успешный lock синхронизирует память, которая была изменена до вызова предыдущего unlock.
std::promise::set_value vs std::future::wait
И так далее. Полный список можно найти в книге [1].
Заключение
Атомарные и неатомарные операции
Перевод статьи Джефа Прешинга Atomic vs. Non-Atomic Operations. Оригинальная статья: http://preshing.com/20130618/atomic-vs-non-atomic-operations/
В Сети уже очень много написано об атомарных операциях, но в основном авторы рассматривают операции чтения-модификации-записи. Однако, существуют и другие атомарные операции, например, атомарные операции загрузки (load) и сохранения (store), которые не менее важны. В этой статье я сравню атомарные загрузки и сохранения с их неатомарными аналогами на уровне процессора и компилятора C/C++. По ходу статьи мы также разберемся с концепцией «состояния гонок» с точки зрения стандарта C++11.
Операция в общей области памяти называется атомарной, если она завершается в один шаг относительно других потоков, имеющих доступ к этой памяти. Во время выполнения такой операции над переменной, ни один поток не может наблюдать изменение наполовину завершенным. Атомарная загрузка гарантирует, что переменная будет загружена целиком в один момент времени. Неатомарные операции не дают такой гарантии.
Без подобных гарантии неблокирующее программирование было бы невозможно, поскольку было бы нельзя разрешить нескольким потокам оперировать одновременно одной переменной. Мы можем сформулировать правило:
В любой момент времени когда два потока одновременно оперируют общей переменной, и один из них производит запись, оба потока обязаны использовать атомарные операции.
Если вы нарушаете это правило, и каждый поток использует неатомарные операции, вы оказываетесь в ситауции, которую стандарт C++11 называет состояние гонок по данным (data race) (не путайте с похожей концепцией из Java, или более общим понятием состояния гонок (race condition)). Стандарт C++11 не объясняет, почему состояние гонок плохо, однако утверждает, что в таком состоянии вы получите неопределенное поведение (§1.10.21). Причина опасности таких состояний гонок, однако, очень проста: в них операции чтения и записи разорваны (torn read/write).
Операция с памятью может быть неатомарной даже на одноядерном процессоре только потому, что она использует несколько инструкций процессора. Однако и одна инструкция процессора на некоторых платформах также может быть неатомарной. Поэтому, если вы пишите переносимый код для другой платформы, вы никак не можете опираться на предположение об атомарности отдельной инструкции. Давайте рассмотрим несколько примеров.
Неатомарные операции из нескольких инструкций
Допустим, у нас есть 64-битная глобальная переменная, инициализированная нулем.
В какой-то момент времени мы присвоим ей значение:
Если мы скомпилируем этот код с помощью 32-битного компилятора GCC, мы получим такой машинный код:
Видно, что компилятор реализовал 64-битное присваивание с помощью двух процессорных инструкций. Первая инструкция присваивае нижним 32 битам значение 0x00000002, и вторая заносит в верхние биты значение 0x00000001. Очевидно, что такое присваивание неатомарно. Если к переменной sharedValue одновременно пытаются получить доступ различные потоки, можно получить несколько ошибочных ситуаций:
Параллельное чтение из sharedVariable также имеет свои проблемы:
Здесь таким же образом компилятор реализует чтение двумя инструкциями: сначала нижние 32 бита считываются в регистр EAX, а потом верхние 32 бита считываются в EDX. В этом случае, если параллельная запись будет произведена между этими двумя инструкциями, мы получим разорванную операцию считывания, даже если запись была атомарной.
Эти проблемы отнюдь не теоретические. Тесты библиотеки Mintomic включает тест test_load_store_64_fail, в котором один поток сохраняет набор 64-битных значений в переменную используя обычный оператор присваивания, а другой поток производит обычную загрузку из той же самой переменной, проверяя результат каждой операции. В многопоточном режиме x86 этот тест ожидаемо падает.
Неатомарные инструкции процессора
Операция с памятью может быть неатомарной даже если она выполняется одной инструкцией процессора. Например, в наборе инструкций ARMv7 есть инструкция strd, которая сохраняет содержимое двух 32-битных регистров в 64-битной переменной в памяти.
На некоторых ARMv7 процессорах эта инструкция не является атомарной. Когда процессор видит такую инструкцию, он на самом деле выполняет две отдельные операции (§A3.5.3). Как и в предыдущем примере, другой поток, выполняющийся на другом ядре, может попасть в ситуацию разорванной записи. Интересно, что ситуация разорванной записи может возникнуть и на одном ядре: системное прерывание — скажем, для запланированной смены контекста потока — может возникнуть между внутренними операциями 32-битного сохранения! В этом случае, когда поток возобновит свою работу, он начнет выполнять инструкцию strd заново.
Другой пример, всем известная операция архитектуры x86, 32-битная операция mov атомарна в том случае, когда операнд в памяти выровнен, и не атомарна в противном случае. То есть, атомарность гарантируется только в случае, когда 32-битное целое число находится по адресу, который делится на 4. Mintimoc содержит тестовый пример test_load_store_32_fail, который проверяет это условие. Этот тест всегда выполняется успешно на x86, но если его модифицировать так, чтобы переменная sharedInt находилась по невыровненному адресу, тест упадет. На моем Core 2 Quad 6600 тест падает, когда sharedInt разделен между различными линиями кеша:
Думаю, мы рассмотрели достаточно нюансов процессорного выполнения. Давайте взглянем на атомарность на уровне C/C++.
Все операции C/C++ считаются неатомарными
В C/C++ каждая операция считается неатомарной до тех пор, пока другое не будет явно указано прозводителем компилятора или аппаратной платформы — даже обычное 32-битное присваивание.
Стандарты языка ничего не говорят по поводу атомарности в этом случае. Возможно, целочисленное присваивание атомарно, может быть нет. Поскольку неатомарные операции не дают никаких гарантий, обычное целочисленное присваивание в C является неатомарным по определению.
На практике мы обычно обладаем некоторой информацией о платформах, для которых создается код. Например, мы обычно знаем, что на всех современных процессорах x86, x64, Itanium, SPARC, ARM и PowerPC обычное 32-битное присваивание атомарно в том случае, если переменная назначения выровнена. В этом можно убедиться, перечитав соответствующий раздел документации процессора и/или компилятора. Я могу сказать, что в игровой индустрии атомарность очень многих 32-битных присваиваний гарантируется этим конкретным свойством.
Как бы там ни было, при написании действительно переносимого кода C и C++, мы следуем давно установившейся традиции считать, что мы не знаем ничего более того, что нам говорят стандарты языка. Переносимые C и C++ спроектированы так, чтобы выполнятся на любом возможном вычислительном устройстве прошлого, настоящего и будущего. Я, например, люблю представлять устройство, память которого можно менять только предварительно заполнив ее случайным мусором:
На таком устройстве вы уж точно не захотите произвести параллальное считывание, так же как и обычное присваивание, потому что слишком высок риск получить в результате случайное значение.
В C++11 наконец-то появился способ выполнять действительно переносимые атомарные сохранения и загрузки. Эти операции, произведенные с помощью атомарной библиотеки C++11 будут работать даже на условном устройстве, описанном ранее: даже если это будет означать, что библиотеке прийдется блокировать мьютекс для того, чтобы сделать каждую операцию атомарной. Моя библиотека Mintomic которую я выпустил недавно, не поддерживает такое количество различных платформ, но работает на некоторых старых компьютерах, оптимизирована вручную и гарантировано неблокирующая.
Расслабленные (relaxed) атомарные операции
Давайте вернемся к примеру с sharedValuem который мы рассматривали в начале. Давайте перепишем его с использованием Mintomic так, чтобы все операции выполнялись атомарно на каждой платформе, которую поддерживает Mintomic. Для начала мы объявим sharedValue как один из атомарных типов Mintomic:
Тип mint_atomic64_t гарантирует корректное выравнивание в памяти для атомарного доступа на каждой платформе. Это важно, поскольку, например, компилятор gcc 4.2 для ARM в среде разработки Xcode 3.2.5 не гарантирует, что тип uint64_t будет выровнен на 8 байтов.
В функции storeValue вместо выполнения обычного неатомарного присваивания, мы должны выполнить mint_store_64_relaxed.
Аналогично, в loadValue мы вызываем mint_load_64_relaxed.
Если использовать терминологию C++11, то эти функции сейчас свободны от состояний гонок по данным (data race free). Если они будут вызваны одновременно, абсолютно невозможно оказаться в ситуации разорванного чтения или записи, независимо от того, на какой платформе выполняется код: ARMv6/ARMv7(режимы Thumb или ARM), x86, x64 или PowerPC. Если вам интересно как работают mint_load_64_relaxed и mint_store_64_relaxed, то обе функции используют инструкцию cmpxchg8b на платформе x86. Подробности реализации для других платформ можно найти в реализации Mintomic.
Вот такой же код с использованием стандартной библиотеки C++11:
Вы должны были заметить, что оба примера используют расслабленные атомарные операции, что подтверждается суффиксом _relaxed в идентификаторах. Этот суффикс напоминает об определенных гарантиях относительно упорядочивания памяти (memory ordering).
В частности, для таких операций допукается переупорядочивание операций с памятью в соответствии с переупорядочиванием компилятором либо с переупорядочиванием памяти процессором. Компилятор даже может оптимизировать избыточные атомарные операции, так же как и неатомарные. Но во всех этих случаях атомарность оперций сохраняется.
Я думаю, что в случае выполнения параллельных операций с памятью, использование функций атомарных библиотек Mintomic или C++11 является хорошей практикой, даже если вы уверены, что обычные операции чтения либо записи будут атомарны на спользуемой вами платформе. Использование атомарных библиотек будет служить лишним напоминанием, что переменные могут быть использованы в конкурентной среде.
Надеюсь, теперь вам стало понятнее, почему Самая простая в мире неблокирующая хэш-таблица использует Mintomic для манипуляции общей памятью одновременно с другими потоками.
Об авторе. Джефф Прешинг работает архитектором ПО в игровой компании Ubisoft и специализируется на многопоточном программировании и неблокирующих алгоритмах. В этом году он делал доклад о многопоточной разработке игр в соответствии со стандартом С++11 на конференции CppCon, видео этого доклада было и на Хабре. Он ведет интересный блог Preshing on Programming, посвященный в том числе и тонкостям неблокирующего программирования и связанных с ним нюансов C++.
Я бы хотел много статей из его блога перевести для сообщества, но поскольку его записи часто ссылаются одна на другую, выбрать статью для первого перевода достаточно сложно. Я попытался выбрать такую статью, которая бы минимально базировалась на других. Хотя рассматриваемый вопрос достаточно прост, я надеюсь, он все же будет интересен многим, кто начинает знакомиться с многопоточным программированием в C++.
Атомарный релиз что такое
Атомарные проверки
Привет, народ! В эфире #радиоСаня и сегодня мы поговорим с вами про атомарные проверки.
Но для начала давайте вспомним про цели тест-дизайна, их всего две:
1. Надизайнить (написать) тесты, которые обнаружат самые серьезные (критичные) ошибки продукта.
2. Уменьшить число тестов, необходимых для нахождения большинства серьезных ошибок.
Вы думаете, что тестов много не бывает? Еще как бывает. Бывает столько, что вы не успеваете их пройти.
То есть, наша с вами задача — написать как можно меньше лучших тестов, которые бы ловили как можно больше серьезных ошибок. Ага, а причем тут “атомарки”?
“Атомарки” или атомарные проверки — это одна из техник тест-дизайна. Она (техника) позволяет делать тесты умными, уменьшая их количество. Атомарное условие — такое условие, над которым невозможно провести дальнейшую декомпозицию (кстати про декомпозицию прикольно рассказывает Нина в своем третьем вебинаре на курсе ПОИНТ. Например, условие, которое не содержит два или более одинарных условия, соединенных логическим оператором (И, ИЛИ, Исключающее ИЛИ).
Как это работает?
— выписываем действия, их параметры и значения (со значениями очень хорошо помогают техники классов эквивалентности и граничных значений);
— не забывайте, что для каждого действия будет свой отдельный “тестовый набор” с параметрами и значениями;
— собираем все выписанное в табличку для наглядности.
Давайте возьмем чего-нибудь для примера. Ну, скажем, какой-нибудь загрузчик фотографий.
Что нам важно выписать? Действия продукта, параметры их действий и значения параметров.
И давайте работать с действием “загружает фотографии” и его параметрами и значениями. Получается, у нас три параметра и восемь значений, отменно.
Собираем таблицу, сначала выписывая самый рабочий, самый стандартный или часто используемый набор значений, приближенный к нашей жизни, посмотрите на нулевую строку в табличке.
И если эта проверка с этим тестовым набором не работает, то, котаны, алес, надизайнились, у нас какая-то очень серьезная ошибка и дальнейшее тестирование этого функционала даже проводить бессмысленно. А если работает, то продолжаем создавать следующий набор, меняя ОДНО значение одного параметра! Посмотрите на первую строку в таблице, затем — на вторую и третью, чувствуете нашу любовь разницу?
Если “нулевой” и первые два теста прошли успешно, а третий зафейлился, вы сразу поймете, в чем ошибка, потому что третий тест относительно четвертого изменился ровно на одно значение. Элементарнейшая локализация, котаны.
Кстати, общее количество атомарных тестов считается по формуле: сумма значений минус количество параметров. Значений — восемь, параметров — три, в итоге — пять проверок. Особняком стоит наш “нулевой” тест, которым мы проверяем работоспособность тестируемой функциональности.
“Атомарки” — весьма удобная техника, чтобы тестировать нестабильные “сырые” продукты, потому что все причины ошибок мы видим сразу же.
Если ваш тестируемый продукт более зрелый, используйте деревья решений, S&T — based testing (тестирование на основании состояний и переходов) или pairwise. Но это уже совсем другая история, а пока хочу поделиться с вами одним лайфхаком…))
Лайфхак, котаны! В нашем разобранном примере все очень просто: и параметров мало, и их значений не много, но что делать, если у вас 15, 20 или 30 значений? Не паникуйте, также формируйте табличку, также выписывайте “нулевой тест”, а потом составляйте наборы, сначала перебирая все значения первого параметра, затем — все значения второго параметра и так далее. Таким образом, вам не придется “скакать” глазами по лесенкам таблицы и бояться, что вы что-то да пропустили.
Статья написана в соавторстве с Агеевой Ниной.
std::atomic. Модель памяти C++ в примерах
Язык C++ компилируется под множество архитектур, поэтому в вопросе синхронизации данных между потоками в С++11 была добавлена модель памяти, которая обобщает механизмы синхронизации различных архитектур, позволяя генерировать для каждого процессора оптимальный код с необходимой степенью синхронизации.
Отсюда следует несколько важных выводов: модель синхронизации памяти C++ — это «искусственные» правила, которые учитывают особенности различных архитектур процессоров. В модели C++ некоторые конструкции, описанные стандартом как undefined behavior (UB), могут корректно работать на одной архитектуре, но приводить к ошибкам работы с памятью на других архитектурах.
Наша задача, как разработчиков на языке C++, состоит в том, чтобы писать корректный с точки зрения стандарта языка код. В этом случае мы можем быть уверены, что для каждой платформы будет сгенерирован корректный машинный код.
Код каждого потока компилируется и выполняется так, как будто он один в программе. Вся синхронизация данных между потоками возложена на плечи атомиков ( std::atomic ), т.к. именно они предоставляют возможность форсировать «передачу» изменений данных в другой поток. Далее я покажу, что мьютексы ( std::mutex ) и другие многопоточные примитивы либо реализованы на атомиках, либо предоставляют гарантии, семантически похожие на атомарные операции. Поэтому ключом к написанию корректных многопоточных программ является понимание того, как конкретно работают атомики.
Три слона
На мой взгляд, основная проблема с атомиками в C++ состоит в том, что они несут сразу три функции. Так на каких же трех слонах держатся атомики?
Атомики позволяют реализовать… атомарные операции.
Атомики накладывают ограничения на порядок выполнения операций с памятью в одном потоке.
Синхронизируют память в двух и более потоках выполнения.
Рассмотрим простой пример read-modify-write операции, а именно прибавление к числу единицы. Пример 0, link:
Про ограничения на порядок выполнения операций. Когда мы пишем код программы, то предполагаем, что операторы языка будут выполнены последовательно. В реальности же компилятор и в особенности процессор могут переупорядочить команды программы с целью оптимизации. Они это делают с учетом ограничений на порядок записи и чтения в локацию памяти. Например, чтение из локации памяти должно происходить после записи, эти операции нельзя переупорядочить. Применение атомарных операций может накладывать дополнительные ограничения на возможные переупорядочивания операций с памятью.
Про синхронизацию данных между потоками. Если мы хотим изменить данные в одном потоке и сделать так, чтобы эти изменения были видны в другом потоке, то нам необходимы примитивы многопоточного программирования. Фундаментальным таким примитивом являются атомики, остальные, например мьютексы, либо реализованы на основе атомиков, либо повторяют семантику атомиков. Все попытки записывать и читать одни и те же данные из разных потоков без примитивов синхронизации могут приводить к UB.
Случаи, когда синхронизация памяти не требуется:
Если все потоки, работающие с одним участком памяти, используют ее только на чтение
Если разные потоки используют эксклюзивно разные участки памяти
Неделимый, но расслабленный
модификация переменной «появится» в другом потоке не сразу
поток thread2 «увидит» значения одной и той же переменной в том же порядке, в котором происходили её модификации в потоке thread1
порядок модификаций разных переменных в потоке thread1 не сохранится в потоке thread2
Можно использовать relaxed модификатор в качестве счетчика. Пример 1, link:
Использование в качестве флага остановки. Пример 2, link:
Пример неверного использования relaxed в качестве флага готовности данных. Пример 3, link:
Полный порядок
Флаг синхронизации памяти «единая последовательность» (sequential consistency, seq_cst ) дает самые строгие. Его свойства:
порядок модификаций разных атомарных переменных в потоке thread1 сохранится в потоке thread2
все потоки будут видеть один и тот же порядок модификации всех атомарных переменных. Сами модификации могут происходить в разных потоках
Таким образом можно представить seq_cst операции, как барьеры памяти, в которых состояние памяти синхронизируется между всеми потоками программы.
Этот флаг синхронизации памяти в C++ используется по умолчанию, т.к. с ним меньше всего проблем с точки зрения корректности выполнения программы. Но seq_cst является дорогой операцией для процессоров, в которых вычислительные ядра слабо связаны между собой в плане механизмов обеспечения консистентности памяти. Например, для x86-64 seq_cst дешевле, чем для ARM архитектур.
Продемонстрируем второе свойство. Пример 4, из книги [1], link:
Синхронизация пары. Acquire/Release
Флаг синхронизации памяти acquire/release является более тонким способом синхронизировать данные между парой потоков. Два ключевых слова: memory_order_acquire и memory_order_release работают только в паре над одним атомарным объектом. Рассмотрим их свойства:
модификация атомарной переменной с release будет видна видна в другом потоке, выполняющем чтение этой же атомарной переменной с acquire
Рассмотрим такой пример, называемый Double Checked Locking Anti-Pattern из [2]. Пример 6, link:
Рассмотрим следующую последовательность действий во времени:
lock мьютекса ( acquire )
unlock мьютекса ( release )
2. далее в игру вступает thread2 :
if(initalized) возвращает true (память, где содержится initialized могла быть неявно синхронизирована между ядрами процессора)
singleton->do_job() приводит к segmentation fault (указатель singleton не обязан был быть синхронизирован с потоком thread1 )
Этот случай интересен тем, что наглядно показывает роль мьютекса не только как примитива синхронизации потока выполнения, но и синхронизации памяти.
Семантика acquire/release классов стандартной библиотеки
Механизм acquire/release поможет понять гарантии синхронизации памяти, которые предоставляют классы стандартной библиотеки для работы с потоками. Ниже приведу список наиболее часто используемых операций.
std::thread::(constructor) vs функция потока
Вызов конструктора объекта std::thread ( release ) синхронизирован со стартом работы функции нового потока ( acquire ). Таким образом функция потока будет видеть все изменения памяти, которые произошли до вызова конструктора в исходном потоке.
std::thread::join vs владеющий поток
После успешного вызова join поток, в котором был вызван join, «увидит» все изменения памяти, которые были выполнены завершившимся потоком.
std::mutex::lock vs std::mutex::unlock
успешный lock синхронизирует память, которая была изменена до вызова предыдущего unlock.
std::promise::set_value vs std::future::wait
И так далее. Полный список можно найти в книге [1].