Почему std:: atomic инициализация не выполняет атомарную версию, поэтому другие потоки могут видеть инициализированное значение?

196
30

Что-то очень странное появилось во время дезинфекции нити предлагаемого boost:: concurrent_unordered_map и рассказанного в этом сообщении в блоге. Короче говоря, bucket_type выглядит так:


  struct bucket_type_impl
{
spinlock<unsigned char> lock; // = 2 if you need to reload the bucket list
atomic<unsigned> count; // count is used items in there
std::vector<item_type, item_type_allocator> items;
bucket_type_impl() : count(0), items(0) { }
...

Тем не менее, нить sanitiser утверждает, что между конструкцией bucket_type и ее первым использованием существует расы, в частности, когда загружается атом отсчета. Оказывается, если вы инициализируете std:: atomic < > через свой конструктор что инициализация не является атомарной, и поэтому местоположение памяти не выделяется атомом и поэтому не отображается к другим потокам, что противоречит интуиции, учитывая, что это атомный, и что большинство атомных операций по умолчанию - memory_order_seq_cst. Поэтому вы должны явно создать хранилище релизов после построения, чтобы инициализировать атом, со значением, видимым для других потоков.


Есть ли какая-то чрезвычайно насущная причина, почему std:: atomic с конструктором, потребляющим значение, не инициализирует себя семантикой выпуска? Если нет, я думаю, что это дефект библиотеки.


Изменить: Ответ Джонатана лучше для истории о том, почему, но ecatmur ответит на ссылки на сообщение об ошибке Alastair по этому вопросу и как он был закрыт, просто добавив примечание к предложениям по строительству нет видимости для других потоков. Поэтому я вручу ответ ecatmur. Спасибо всем, кто ответил, я думаю, что понятно, что нужно спросить о дополнительном конструкторе, он, по крайней мере, будет выделяться в документации, что есть что-то необычное в конструкторе потребления стоимости.


Изменить 2: Я закончил тем, что поднял это как недостаток на языке С++ с комитетом, а Ханс Бем, который возглавляет часть Concurrency, считает, что это не проблема по следующим причинам:


    Никакой существующий компилятор С++ в 2014 году не рассматривает, как потреблять. Поскольку в коде реального мира вы никогда не перейдете атом к другому потоку, не пройдя какой-либо выпуск/приобретать, инициализация атома будет видна для всех потоков с использованием атома. Я думаю, это прекрасно, пока компиляторы не догонят, и до этого Thread Sanitiser предупредит об этом.


    Если вы выполняете несогласованную покупку-получение-релиз, как я, (я использую атом-релиз-внутри-блокировки/потребления-вне-блокировки, чтобы умозрительно избегать использования спин-блокировки, освобождающей релиз, где это было необязательно) тогда вы достаточно большой мальчик, чтобы знать, что вы должны вручную хранить выпускные атомы после строительства. Это, вероятно, справедливая точка.


спросил(а) 2021-01-19T16:12:49+03:00 6 месяцев назад
1
Решение
166

Это потому, что конструктор преобразования constexpr, а функции constexpr не могут иметь побочные эффекты, такие как атомная семантика.


В DR846 Аластер Мередит пишет:


Я не уверен, что инициализация подразумевается с помощью ключевого слова constexpr (который ограничивает форму конструктора), но даже если это так, я думаю, что это стоит явно указать, поскольку вывод будет слишком тонкий в этом случае.



Разрешение для этого дефекта (Лоуренсом Кроулом) заключалось в том, чтобы задокументировать конструктор с примечанием:


[Примечание: конструкция не является атомарной. -end note]


Затем записка была расширена до текущей формулировки, давая пример возможной расы памяти (через операции memory_order_relaxed, сообщающие адрес атома) в DR1478.


Причина, по которой конструктор преобразования должен быть constexpr, является (прежде всего), чтобы разрешить статическую инициализацию. В DR768 мы видим:


Дальнейшее обсуждение: почему ctor помечен как "constexpr"? Лоуренс [Кроуль] сказал, что это позволяет статически инициализировать объект, и это важно, потому что в противном случае было бы условие гонки при инициализации.



Итак: создание конструктора constexpr устраняет условия гонки на объектах статического времени жизни за счет гонки в объектах с динамическим жизненным циклом, что происходит только в довольно надуманных ситуациях, так как для гонки должно быть место памяти динамический ресурс атома должен быть передан другому потоку таким образом, чтобы не приводить к тому, что значение атомарного объекта также синхронизируется с этим потоком.

ответил(а) 2021-01-19T16:12:49+03:00 6 месяцев назад
161

Это преднамеренный выбор дизайна (там даже примечание в стандартном предупреждении об этом), и я думаю, что это было сделано в попытке быть совместимым с C.


Атомные атомы С++ 11 были сконструированы таким образом, чтобы они могли также использоваться WG14 для C, используя не-членные функции, такие как atomic_load с типами, такими как atomic_int, а не с функциями-членами С++ - только std::atomic<int>. В исходном дизайне тип atomic_int не имеет специальных свойств, а атомарность достигается только через atomic_load() и другие функции. В этой модели atomic_init не является атомной операцией, она просто инициализирует POD. Только последующий вызов atomic_store(&i, 1) был бы атомарным.

В конце концов, WG14 решила сделать что-то по-другому, добавив спецификатор _Atomic, который делает тип atomic_int магическим. Я не уверен, означает ли это, что инициализация атома C может быть атомарной (как она есть, atomic_init в C11 и С++ 11 документирована как неатомная), поэтому, возможно, правило С++ 11 не нужно. Я подозреваю, что люди будут утверждать, что есть хорошая причина в производительности, чтобы сохранить инициализацию неатомной, как сказано выше в комментарии к interjay, вам нужно отправить некоторое уведомление другому потоку, который построил obejct и был готов к чтению, так что уведомление могло ввести необходимое ограждение. Выполнение его один раз для инициализации std::atomic, а затем второй раз, чтобы сказать, что объект сконструирован, может быть расточительным.

ответил(а) 2021-01-19T16:12:49+03:00 6 месяцев назад
110

Я бы сказал, это потому, что конструкция никогда не связана с потоковой связью: когда вы строите объект, вы заполняете ранее неинициализированную память разумными значениями. Нет другого способа определить, завершена ли эта операция, если только она не будет передана потоком конструирования напрямую. Если вы все равно участвуете в гонках со строительством, у вас сразу есть поведение undefined.


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

ответил(а) 2021-01-19T16:12:49+03:00 6 месяцев назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема