Требуется ли для зависимых чтений загрузка-получение?

Выявляет ли следующая программа гонку данных или любую другую проблему параллелизма?

#include <cstdio>
#include <cstdlib>

#include <atomic>
#include <thread>

class C {
public:
    int i;
};

std::atomic<C *> c_ptr{};

int main(int, char **) {
    std::thread t([] {
        auto read_ptr = c_ptr.load(std::memory_order_relaxed);
        if (read_ptr) {
            // does the following dependent read race?
            printf("%d\n", read_ptr->i);
        }
    });

    c_ptr.store(new C{rand()}, std::memory_order_release);

    return 0;
}

Годболт

Меня интересует, нужна ли при чтении указателей семантика загрузки-получения при загрузке указателя, или же природа чтения указателей с зависимым чтением делает этот порядок ненужным. Если это важно, предположим, что это arm64, и, если возможно, опишите, почему это важно.

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

Я не уверен, в чем вопрос? данные (указатель), получаемые от atomic, являются потокобезопасными. и вы получаете доступ к 2 разным указателям. (одна из них по умолчанию построена (неинициализирована) атомарно)

apple apple 20.11.2022 19:28

Технически, да. На практике вряд ли имеет значение. Я сомневаюсь, что есть архитектура, в которой этот конкретный случай может вызвать проблемы без ограждений освобождения/приобретения, хотя в более сложном случае это определенно может иметь большое значение даже на практике. Ограждения освобождения/получения памяти действительно гарантируют, что i будет правильно прочитан - это их точка зрения, что другие данные, кроме значения указателя, правильно прочитаны/записаны.

ALX23z 20.11.2022 19:54

Это дубликат stackoverflow.com/questions/51468730/…, но, видимо, мне не разрешено помечать его как таковой.

Brian Bi 20.11.2022 23:31

@ ALX23z RE «... без ограждений освобождения / получения ...» Я считаю, что вам абсолютно необходимо освобождение записи, без него вы можете читать неинициализированную память вместо результата rand (). Я считаю, что это из-за опыта, и если вы учтете возможности переупорядочения, запись в i может быть переупорядочена WRT для записи в c_ptr.

nmr 20.11.2022 23:42

@BrianBi связанный с вами вопрос записывается в указатель с помощью std::memory_order_relaxed, этот вопрос записывается с помощью std::memory_order_release. Они похожи, так что спасибо за ссылку.

nmr 20.11.2022 23:47

Хорошая мысль, хотя это ничего не меняет, потому что memory_order_release не имеет эффекта, если нет соответствия memory_order_acquire или сильнее (я собираюсь притвориться, что memory_order_consume не существует)

Brian Bi 20.11.2022 23:56

@BrianBi «memory_order_release не действует, если нет соответствующего memory_order_acquire [...]», вы уверены? У вас есть цитата или что-то еще? Мне это кажется большой претензией.

nmr 20.11.2022 23:58

@nmr в этом есть зависимость вновь выделенной памяти. Это само по себе имеет определенные последствия для памяти.

ALX23z 21.11.2022 07:21

@BrianBi, который release требует обработки acquire, чтобы что-то сделать, - это неправильно. (1) Операция делает определенные вещи и имеет последствия даже без соответствия acquire. (2) это больше модель памяти C++, на практике операция может фактически не делать ничего, кроме атомарной расслабленной операции на некоторых архитектурах.

ALX23z 21.11.2022 07:26

@ ALX23z: На практике компиляторы C++ реализуют release одинаково независимо от контекста и без проверки всей программы на наличие возможного средства чтения как минимум с порядком consume. Но поскольку стандарт ISO C++ ничего не гарантирует, нет, никаких гарантий нет. Раньше ничего не происходит, поэтому нет гарантированного порядка в абстрактной машине С++. Только в некоторых, но не во всех конкретных реализациях на ассемблере для определенных машин реальными компиляторами, которые решили не искать этот UB и специально ломать код.

Peter Cordes 21.11.2022 17:12

@ ALX23z Похоже, вы говорите, что операция с семантикой выпуска по-прежнему имеет эффект, даже если нет соответствующего приобретения. Это правда, конечно. Я имел в виду, что нет никакой разницы между выпуском и расслаблением, если нет соответствующего приобретения, поэтому нет существенной разницы между этим вопросом и другим, который я связал.

Brian Bi 22.11.2022 00:16

@BrianBi нет, даже без сопоставления получения операция выпуска выполняет другую операцию, чем операция расслабления. Что именно происходит, во многом зависит от аппаратного обеспечения. Но даже не вникая в аппаратные нюансы, добавляет различные ограничения на оптимизации компилятора.

ALX23z 22.11.2022 05:53

@ ALX23z Я говорю только с точки зрения того, что гарантирует стандарт C++.

Brian Bi 22.11.2022 15:49

@BrianBi даже с точки зрения release накладывает другие ограничения, чем relaxed. Возможно, эти ограничения не очень полезны, если не вызывается acquire, но они влияют на то, как компилятор может скомпилировать код.

ALX23z 22.11.2022 16:42

@ ALX23z ALX23z Если вы считаете, что есть разница, объясните, в чем она заключается, и сошлитесь на стандарт.

Brian Bi 22.11.2022 17:37

@BrianBi говорит, что это влияет на ограничения операций с памятью. С расслабленным компилятором, свободным для переупорядочения операций, скажем, у вас есть op(); x.store(1,relaxed); пост-оптимизация, которая может стать x.store(1,relaxed); op();, это невозможно с операцией выпуска. Все записи op() должны быть запланированы до магазина x.

ALX23z 22.11.2022 22:49

@ ALX23z И как бы вы заметили, произошло ли такое изменение порядка? Вы могли бы наблюдать это, читая из x в другом потоке. И если это чтение расслаблено, то оно может не увидеть результатов op(), даже если магазин был релизным.

Brian Bi 23.11.2022 00:28

@BrianBi это может повлиять на производительность или вызвать сбой кода. Скажем, op() занимает много времени, и какой-то другой поток тестирует его с ослабленными проверками атомарной переменной. Как только обнаруживается, что переменная верна, она начинает выполнять что-то еще. В такой ситуации переупорядочивание приведет к сбоям в работе кода/работе не так, как предполагалось.

ALX23z 23.11.2022 03:01

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

Brian Bi 23.11.2022 05:21

@BrianBi нет, это не может произойти с release порядком памяти, он запрещает любую операцию сохранения из op() после выпуска. Я не говорю о том, видно ли то, что делает op() — это не имеет значения. Я говорю о том, когда поток запланировал операцию. При чтении в расслабленном состоянии поток узнает, что если он выполнит чтение с получением, то операция op() будет видна. Иногда видимость изменений не нужна, достаточно знать, что они готовы.

ALX23z 23.11.2022 08:19

Порядок операций @BrianBi в потоке может быть важен при работе с атомарными элементами. В этом весь смысл consume того, что он действует как забор для чтения потока, в отличие от расслабленного, при этом не фиксируя и не обновляя ничего, что делает выпуск и получение.

ALX23z 23.11.2022 09:51

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

Brian Bi 23.11.2022 13:36

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

ALX23z 23.11.2022 13:54

@ ALX23z Как именно, по вашему мнению, вы можете наблюдать за порядком?

Brian Bi 23.11.2022 14:26

@BrianBi, другой поток, который проверил переменную, заметил, что она верна, начал выполнять тяжелые вычисления. А в диспетчере задач можно заметить больше ядер, получающих 100%. Это довольно заметно, нет?

ALX23z 23.11.2022 14:34

@ ALX23z ALX23z Это не считается наблюдаемым эффектом в стандарте. Выполнение ввода-вывода будет наблюдаемым эффектом, и если op() также выполняет ввод-вывод, он может быть переупорядочен после выпуска хранилища.

Brian Bi 23.11.2022 14:48

@BrianBi изменение атомарной переменной всегда является наблюдаемым поведением с побочными эффектами. Это был очевидный пример того, почему это очень важно. Обычно оптимизация или переупорядочивание любых таких операций запрещены, как в случае с volatile, однако правила упорядочивания памяти разрешают это. По этой причине был придуман термин «видимый».

ALX23z 23.11.2022 15:05

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

Brian Bi 23.11.2022 15:19

@BrianBi, если другой поток прочитал часть данных, как это не наблюдаемое поведение? Это похоже на то, что вы пытаетесь сделать вид, что вся память C++ не является частью стандарта и недоступна для наблюдения.

ALX23z 23.11.2022 15:55

@ ALX23z Боюсь, что сейчас мы спорим об определениях. Я хочу сказать, что, опять же, если поток 1 выполняет ввод-вывод, за которым следует освобождение хранилища в x, а поток 2 выполняет расслабленную загрузку x, которая считывает сохраненное значение, а затем выполняет ввод-вывод, могут произойти две операции ввода-вывода. в любом порядке. Если вы с этим согласны, значит я прав, что магазин релизов не мешает переупорядочивать. Если вы не согласны, то объясните, почему.

Brian Bi 23.11.2022 16:09

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

ALX23z 23.11.2022 16:50

@BrianBi в качестве примера позвольте мне написать некоторый тривиальный код: atomic<int> x{0}, y{0}; поток 1: x.store(1,relaxed); y.store(1,release); поток 2: if(y.load(relaxed) == 1){assert(x.load(relaxed)==1);} если вы измените выпуск на ослабленный, утверждение может сработать. И нет соответствующего приобретения.

ALX23z 23.11.2022 16:53

@ ALX23z Это утверждение может запускаться как есть. Разницы не будет, если релиз поменять на расслабленный.

Brian Bi 23.11.2022 17:18

с инструкцией по выпуску. Релиз требует, чтобы сохранение x происходило после сохранения y. Условие if предполагает, что загрузка x происходит после загрузки y.

ALX23z 23.11.2022 18:41

@ ALX23z Компилятору разрешено переупорядочивать хранилища, поскольку нет связи синхронизации с другим потоком. Следовательно, утверждение может сработать. Хранилище релиза не предотвращает такое переупорядочивание, ЕСЛИ нет соответствующей загрузки для получения. Пожалуйста, прочтите стандарт, если не верите мне.

Brian Bi 23.11.2022 22:26

@ ALX23z То, что ни один из известных нам компиляторов не может сломать ваш пример кода и заставить его утверждать, не означает, что такой компилятор не может быть 1) соответствующим стандарту 2) программируемым (не слишком сложно сделать) 3) полезным в реальном мире. Мы даже не говорим об абстрактных академических упражнениях. Первые поколения компиляторов рассматривали атомарные операции как непрозрачные забавные вызовы, выполняя нулевое преобразование вокруг них, но теперь они выполняют множество преобразований кода.

curiousguy 26.11.2022 05:10
Шаблоны Angular PrimeNg
Шаблоны Angular PrimeNg
Как привнести проверку типов в наши шаблоны Angular, использующие компоненты библиотеки PrimeNg, и настроить их отображение с помощью встроенной...
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Если вы веб-разработчик (или хотите им стать), то вы наверняка гик и вам нравятся "Звездные войны". А как бы вы хотели, чтобы фоном для вашего...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Начала с розового дизайна
Начала с розового дизайна
Pink Design - это система дизайна Appwrite с открытым исходным кодом для создания последовательных и многократно используемых пользовательских...
Шлюз в PHP
Шлюз в PHP
API-шлюз (AG) - это сервер, который действует как единая точка входа для набора микросервисов.
14 Задание: Типы данных и структуры данных Python для DevOps
14 Задание: Типы данных и структуры данных Python для DevOps
проверить тип данных используемой переменной, мы можем просто написать: your_variable=100
1
36
118
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваш код небезопасен и может сломаться на практике с настоящими компиляторами для DEC Alpha AXP (что может нарушить причинно-следственную связь из-за хитрых махинаций с кэш-банком IIRC).


Что касается стандарта ISO C++, гарантирующего что-либо в абстрактной машине C++, нет, нет никакой гарантии, потому что ничто не создает отношения «происходит до» между инициализацией int и чтением в другом потоке.

Но на практике компиляторы C++ реализуют release одинаково независимо от контекста и без проверки всей программы на наличие возможного считывателя как минимум с порядком consume.

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


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

Учитывая текущее устаревшее состояние consume, единственный способ получить эффективный ассемблер на ISA с порядком зависимостей (не Alpha), но которые не получают бесплатно (x86), — это написать такой код. Ядро Linux делает это на практике для таких вещей, как RCU.

Это требует, чтобы он был достаточно простым, чтобы компиляторы не могли нарушить порядок зависимостей, например. доказывая, что любой не-NULL read_ptr будет иметь определенное значение, например адрес статической переменной.

Смотрите также

  • Что на самом деле делает memory_order_consume?
  • C++11: разница между memory_order_relaxed и memory_order_consume
  • Порядок потребления памяти в C11 — подробнее об аппаратном механизме / гарантии того, что потребление предназначено для программного обеспечения. Exec в любом случае может только переупорядочить независимую работу, не запуская загрузку до того, как станет известен адрес загрузки, поэтому на большинстве процессоров принудительное упорядочение зависимостей происходит бесплатно в любом случае: только несколько моделей DEC Alpha могут нарушать причинно-следственную связь и эффективно загружать данные. до того, как у него был указатель, который дал ему адрес.
  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r1.html - и другие документы C++ WG21, связанные с этим, о том, почему потребление не рекомендуется.

Мое единственное сожаление в жизни состоит в том, что я могу проголосовать за это только один раз.

nmr 21.11.2022 18:11

Другие вопросы по теме