Два разных процесса с двумя переменными std :: atomic на одном адресе?

Я прочитал стандарт C ++ (n4713) § 32.6.1 3:

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

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

Скажем, у меня есть именованный сегмент разделяемой памяти в Linux (через shm_open () и mmap ()). Как, например, выполнить операцию без блокировки на первых 4 байтах сегмента разделяемой памяти?

Сначала я подумал, что могу просто reinterpret_cast указать указатель на std::atomic<int32_t>*. Но потом я прочитал это. Во-первых, он указывает на то, что std :: atomic может не иметь того же размера T или выравнивания:

Когда мы разрабатывали атомарные модели C ++ 11, у меня было неправильное представление о том, что можно полупортативно применять атомарные операции к данным, не объявленным атомарными, используя такой код, как

int x; reinterpret_cast<atomic<int>&>(x).fetch_add(1);

Это явно не сработает, если представления atomic и int различаются или их выравнивание различается. Но я знаю, что это не проблема для платформ, которые меня волнуют. И на практике я могу легко проверить наличие проблемы, проверив во время компиляции соответствие размеров и выравнивания.

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

Однако не гарантируется, что это будет надежно даже на платформах, на которых можно было бы ожидать, что это будет работать, поскольку это может запутать анализ псевдонимов на основе типов в компиляторе. Компилятор может предположить, что int также не доступен как atomic<int>. (См. 3.10, [Basic.lval], последний абзац.)

Любой вклад приветствуется!


person HCSF    schedule 08.07.2018    source источник


Ответы (3)


Стандарт C ++ не касается нескольких процессов, поэтому формальных ответов быть не может. В этом ответе предполагается, что программа в отношении синхронизации ведет себя более или менее так же, как с процессами, так и с потоками.

Первое решение требует C ++ 20 atomic_ref

void* shared_mem = /* something */

auto p1 = new (shared_mem) int;  // For creating the shared object
auto p2 = (int*)shared_mem;      // For getting the shared object

std::atomic_ref<int> i{p2};      // Use i as if atomic<int>

Это предотвращает присутствие непрозрачных атомарных типов, существующих в разделяемой памяти, что дает вам точный контроль над тем, что именно там происходит.

Решение до C ++ 20 было бы

auto p1 = new (shared_mem) atomic<int>;  // For creating the shared object
auto p2 = (atomic<int>*)shared_mem;      // For getting the shared object

auto& i = *p2;

Или используя C11 atomic_load и _ 5_

volatile int* i = (volatile int*)shared_mem;
atomic_store(i, 42);
int i2 = atomic_load(i);
person Passer By    schedule 08.07.2018
comment
Спасибо за ваш ответ. Ваше решение до C ++ 20 преобразует общую память в std::atomic<int>. В этой статье предлагается выполнить приведение к std::atomic<int> (или std::atomic<int>*) может быть недопустимым / запутать компилятор. Подумал? - person HCSF; 08.07.2018
comment
@HCSF Здесь нет приведения между обычным и атомарным типами, я только получил указатель на ранее созданный атомарный объект. - person Passer By; 08.07.2018
comment
@PasserBy Хороший пост. Кажется, у нас есть два p (в POD ?, смеется). - person Paul Sanders; 09.07.2018
comment
@PaulSanders Пример кода предназначен только для демонстрации, если вы это имеете в виду. - person Passer By; 09.07.2018
comment
@PasserBy Конечно, но он должен хотя бы компилироваться :) Почему бы тебе не исправить? Я нашел это сбивающим с толку, и другие могут тоже. - person Paul Sanders; 09.07.2018
comment
@PasserBy Ах! Я вижу ваши комментарии сейчас. Итак, первая строка использует новое размещение для создания общего объекта в сопоставленной общей памяти, а вторая - для получения созданного общего объекта в сопоставленной общей памяти. Теперь мне просто интересно, работают ли по-прежнему порядки памяти, как определено в стандарте, если мы начнем вызывать _1 _ / _ 2_ на p. - person HCSF; 09.07.2018
comment
@HCSF Конечно. Почему бы им не быть? - person Paul Sanders; 09.07.2018
comment
Я имею в виду, что процесс A вызывает p->store(x, std::memory_order_release), а процесс B вызывает p->load(std::memory_order_acquire) (например), тогда все записи из процесса A до сохранения x будут видны процессу B, когда процесс B вызывает load () (как определено в стандарте)? Причина, по которой я сомневаюсь, заключается в том, что вы также указали во втором предложении. Этот ответ предполагает, что программа ведет себя более или менее так же, как с процессами, так и с потоками в отношении синхронизации. - person HCSF; 09.07.2018
comment
@HCSF Я считаю разумным предположить это, учитывая факторы, способствующие гонке данных, но в случае сомнений всегда проверяйте свою конкретную платформу. - person Passer By; 09.07.2018
comment
Без обид - мне просто интересно, как лучше всего проверить / протестировать. Просто хочу быть уверенным. Я использую x86-64 (точнее, Skylake). - person HCSF; 09.07.2018
comment
@HCSF Загляните в их руководства - person Passer By; 09.07.2018
comment
Это очень толстая. Я просто пытался сделать что-то очень простое. Машинные коды очень простые - в основном 2 mov. Теперь мне интересно, как проявляется эффект std::memory_order_release и std::memory_order_acquire - все записи до выпуска появятся при успешном считывании получения. Я посмотрел раздел mov в мануале. Об этом ничего не говорится. - person HCSF; 09.07.2018
comment
хм ... это не по теме. Спрошу в другом посте. Спасибо за Ваш ответ! - person HCSF; 09.07.2018

Да, в стандарте C ++ обо всем этом нет слов.

Если вы работаете в Windows (чего, вероятно, нет), вы можете использовать _ 1_ и т. д., которые предлагают всю необходимую семантику и не заботятся о том, где находится объект, на который указывает ссылка (это ДЛИННЫЙ *).

На других платформах в gcc есть несколько атомарных встроенных функций что могло бы помочь с этим. Они могут освободить вас от тирании авторов стандартов. Проблема в том, что сложно проверить, является ли полученный код пуленепробиваемым.

person Paul Sanders    schedule 08.07.2018
comment
Да, проблема заключается в тестировании. Вот почему я обратился к стандарту C ++ и надеюсь получить там пуленепробиваемое. - person HCSF; 08.07.2018
comment
Проверьте код, созданный компилятором. Если он использует инструкцию XCHG, тогда вы в безопасности, поскольку это предлагает как атомарность, так и ограждение памяти на аппаратном уровне. - person Paul Sanders; 09.07.2018
comment
Правильно, встроенный gcc должен работать. Фактически, мой код использует встроенные функции gcc. Просто скорость ухудшилась на Skylake (примерно на 40% ниже по сравнению с i7, Ivy и т. Д.). Я сузился до __sync_sychronize(). __sync_sychronize() переводится в mfence на x86_64, и больше ничего. Что касается операций std::atomic, на удивление я не вижу никакого ограждения, поэтому я надеюсь использовать std::atomic, но из-за неясного поведения использования std::atomic между процессами я не решаюсь использовать без подтверждения его поведения. Вот почему появился этот пост :) - person HCSF; 09.07.2018
comment
OK. Вам не нужно это mfence с xchg (это то, что использует std::atomic, IIRC), потому что xchg позаботится об этом за вас. Но @PasserBy дает хороший совет, я бы принял его. Использование размещения new на самом деле является очень изящным решением, и я был бы счастлив использовать его в своем собственном коде. Проголосовали. Мой ответ на самом деле не так уж и актуален. - person Paul Sanders; 09.07.2018
comment
Да, я использую ограждение для другой цели. Тем не менее, его можно заменить на xchg, как вы предлагали, чтобы получить полный барьер на x86-64. Я попробую (префикс lock в инструкции не очень дружелюбный). Да, я думаю, ответ Пассера достаточно хорош. Приму его ответ. - person HCSF; 09.07.2018
comment
OK. Не уверен, что вы имеете в виду под x86-64, но просто для ясности / педантичности XCHG одинаково хорошо работает в 32-битных или 64-битных приложениях. Вам не нужно lock с xchg - вы получите это бесплатно. В целом, это самый простой и дешевый способ реализовать std:;atomic_int на уровне ассемблера, подробнее здесь: stackoverflow.com/questions/19652824/. - person Paul Sanders; 09.07.2018

На всех основных платформах std::atomic<T> имеет тот же размер, что и T, хотя, возможно, более высокие требования к выравниванию, если T имеет alignof ‹sizeof.

Вы можете проверить эти предположения с помощью:

  static_assert(sizeof(T) == sizeof(std::atomic<T>), 
            "atomic<T> isn't the same size as T");

  static_assert(std::atomic<T>::is_always_lock_free,  // C++17
            "atomic<T> isn't lock-free, unusable on shared mem");

  auto atomic_ptr = static_cast<atomic<int>*>(some_ptr);
           // beware strict-aliasing violations
           // don't also access the same memory via int*
           // unless you're aware of possible issues
      // also make sure that the ptr is aligned to alignof(atomic<T>)
      // otherwise you might get tearing (non-atomicity)

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

Или, если все доступы к разделяемой памяти из всех процессов последовательно используют atomic<T>, тогда нет никаких проблем, вам нужно только lock-free, чтобы гарантировать отсутствие адресов. (Вам необходимо проверить это: std :: atomic использует хеш-таблицу блокировок для неблокируемых. Это зависит от адреса, и отдельные процессы будут иметь отдельные хэш-таблицы блокировок.)

person Peter Cordes    schedule 18.12.2019
comment
Спасибо за советы. Я думаю, std::atomic<T>::is_always_lock_free предполагает, что атомарный объект будет правильно выровнен? - person HCSF; 19.12.2019
comment
Считаете ли вы, что решение Passer By C ++ 11 с использованием volatile и atomic_store/load() (я думаю, что atomic_store/load_explicit варианты даже лучше, поскольку они дают более точный контроль над упорядочением памяти) лучше, чем приведение к atomic<int>*? Потому что атомарные функции позаботятся о размере и выравнивании? - person HCSF; 19.12.2019
comment
@HCSF: Да, atomic<T> работает, только если вы удовлетворяете его alignof(atomic<T>) требованию. Текущие реализации не проверяют выравнивание во время выполнения, чтобы увидеть для каждого экземпляра, может ли оно быть свободным от блокировок, поэтому функция-член is_lock_free() может просто возвращать is_always_lock_free. Несовпадающие типы - UB; практический результат для несовпадающего atomic<T> на x86 - потенциально неатомарность (разрыв) для чистой загрузки и чистого хранения и экстремальные потери производительности для атомарного RMW (проверьте счетчики производительности разделенной блокировки). - person Peter Cordes; 19.12.2019
comment
@HCSF: решение PasserBy C11 не требует volatile int*; это бессмысленно и неправильно. Это должно быть atomic_int *p, потому что это то, что вы должны передать atomic_load. volatile int*p не выполняет неявное преобразование в _Atomic int * даже в C. Но да, версии _explicit - это то, что вам нужно для передачи параметра memory_order в C. В C ++ есть перегрузки функций, поэтому вы можете использовать p->load(std::memory_order_relaxed). Но нет, атомарные функции не заботятся о размере и выравнивании, вам нужно передать им выровненный указатель правильного типа. - person Peter Cordes; 19.12.2019
comment
Я просто еще раз посмотрел на эталон. atomic_load/store() и их _explicit варианты на самом деле не принимают volatile int*, но используют только volatile atomic<T>* и atomic<T>* (shared_ptr ‹T› * устарел). Я надеялся, что atomic_load/store_explicit() обнаружит выравнивание и наложит блокировку в случае неправильного выравнивания. Спасибо. - person HCSF; 19.12.2019
comment
@HCSF: нет, это далеко не похоже на то, как что-то работает. Вам действительно не нужны смещенные атомы или обнаружение выравнивания во время выполнения. Инструкции с префиксом lock с разделением строк кэша настолько медленны, что есть событие счетчика производительности, которое просто обнаруживает их на процессорах Intel. См. Почему целочисленное присвоение естественно выровненной переменной является атомарной на x86? для получения информации о требованиях к выравниванию Intel / AMD. И, кстати, atomic_load/store функции принимают только volatile atomic<T>*, поэтому они могут работать как с энергозависимыми, так и с энергонезависимыми указателями. Обычно вы не используете volatile atomic<T>. - person Peter Cordes; 19.12.2019