Я ищу альтернативу std::atomic_ref, которая может быть используется без поддержки C ++ 20. Я рассматривал приведение указателей на std::atomic, но это не кажется безопасным вариантом.

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

Любая помощь приветствуется!

2
D K 20 Май 2021 в 15:57

2 ответа

Лучший ответ

Если ваш компилятор поддерживает OpenMP (большинство из них поддерживает), вы можете пометить доступ к вашему объекту с помощью #pragma atomic. Возможно, с правильной операцией (read, write, update, capture) и семантикой упорядочения памяти.

ИЗМЕНИТЬ

Кроме того, похоже, что Boost предоставляет atomic_ref, доступный для кодов до C ++ 20: https://www.boost.org/doc/libs/1_75_0/doc/html/atomic/interface.html#atomic.interface.interface_atomic_ref

Другой способ - преобразование неатомарного объекта в атомарный с помощью reinterpret_cast. Это решение, скорее всего, вызовет неопределенное поведение, но на самом деле может работать с некоторыми реализациями. Например, он используется в библиотеке Facebook Folly: https://github.com/facebook/folly/blob/master/folly/synchronization/PicoSpinLock.h#L95.

1
Daniel Langr 20 Май 2021 в 13:55

В GCC / clang (и других компиляторах, реализующих расширения GNU C) вы можете использовать __atomic встроенные функции, например

int load_result = __atomic_load_n(&plain_int_var, __ATOMIC_ACQUIRE);

Вот как реализован atomic_ref<T> на таких компиляторах: просто оболочки для этих встроенных команд. (Вот почему atomic_ref очень легкий, и обычно лучше конструировать его бесплатно каждый раз, когда он вам нужен, а не хранить один atomic_ref.)

Поскольку у вас не будет std::atomic_ref<T>::required_alignment, обычно достаточно, чтобы обеспечить естественное выравнивание объектов, т. е. alignas( sizeof(T) ) T foo; убедиться, что операции __atomic действительно атомарны, а также имеют гарантии порядка памяти. (Во многих реализациях все простые T, которые вообще поддерживают безблокировочную атомику, уже получают достаточное выравнивание, но, например, некоторые 32-битные системы выравнивают int64_t только по 4 байтам, но 8-байтовые атомики являются только атомарный с 8-байтовым выравниванием. x86 gcc -m32 имел проблему с этим на C ++ на некоторое время, а для намного дольше с _Atomic в C, окончательно исправлено в 2020 году, хотя затронуло только члены структуры.)


reinterpret_cast< std::atomic<T>* > на практике может работать на большинстве компиляторов , возможно, даже не будучи UB, в зависимости от внутреннего устройства atomic<>.

(Большинство?) Другие компиляторы реализуют атомарный (и атомарный_ref) способом, похожим на GNU C, я думаю, используя встроенные функции. например для MSVC, что-то вроде _InterlockedExchange() для реализации atomic<>::exchange.

В основных реализациях C ++ atomic<T> имеет тот же размер и макет, что и обычный T. (Размер - это то, что вы можете static_assert). Теоретически для неблокирующего atomic<> можно включать мьютекс или что-то в этом роде, но в обычных реализациях этого не делается (Где блокировка для std :: atomic?). (Частично для совместимости с C11 _Atomic, который IIRC предъявляет некоторые требования к тому, чтобы даже неинициализированные или, возможно, инициализированные нулем объекты все еще работали должным образом. Но также только по причинам размера.)

Несмотря на то, что ISO C ++ не гарантирует, что он четко определен, вы в конечном итоге вызовете __atomic_fetch_add_n или InterlockedAdd для переменной-члена int atomic<int> с тем же адресом, что и исходный простой int.

Технически это все еще может быть UB; есть правило о совместимости структур до первого различия в их определении, но я менее уверен насчет int* в структуре или особенно указателя struct{int;}* на объект int. Я думаю, что это нарушает правило строгого псевдонима.

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

Однако наиболее вероятным сценарием поломки будет ситуация, когда та же функция (после встраивания) считывала или записывала простую переменную в сочетании с операциями с той же переменной через atomic<>* или { {X1}} ссылка. Особенно, если нет какого-либо барьера памяти, разделяющего эти обращения, например вызова some_thread.join(). Если вы смешиваете атомарный и неатомарный доступ в одной функции (после встраивания), это может быть безопасным и достаточно переносимым, чтобы работать до тех пор, пока вы не сможете правильно использовать atomic_ref<>.


Другой хороший краткосрочный вариант - вручную напрямую использовать атомарные встроенные модули GNU C или MSVC, если ваш исходный код в настоящее время заботится только об одном или другом. Или используйте собственный (ограниченное подмножество) atomic_ref, используя те версии этих функций, которые вам действительно нужны.

2
Peter Cordes 20 Май 2021 в 17:56