Выявляет ли следующая программа гонку данных или любую другую проблему параллелизма?
#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, и, если возможно, опишите, почему это важно.
Я попытался найти обсуждения зависимых чтений и не нашел явного признания их неявных барьеров переупорядочения загрузки. Мне это кажется безопасным, но я недостаточно доверяю своему пониманию, чтобы знать, что это безопасно.
Технически, да. На практике вряд ли имеет значение. Я сомневаюсь, что есть архитектура, в которой этот конкретный случай может вызвать проблемы без ограждений освобождения/приобретения, хотя в более сложном случае это определенно может иметь большое значение даже на практике. Ограждения освобождения/получения памяти действительно гарантируют, что i
будет правильно прочитан - это их точка зрения, что другие данные, кроме значения указателя, правильно прочитаны/записаны.
Это дубликат stackoverflow.com/questions/51468730/…, но, видимо, мне не разрешено помечать его как таковой.
@ ALX23z RE «... без ограждений освобождения / получения ...» Я считаю, что вам абсолютно необходимо освобождение записи, без него вы можете читать неинициализированную память вместо результата rand (). Я считаю, что это из-за опыта, и если вы учтете возможности переупорядочения, запись в i
может быть переупорядочена WRT для записи в c_ptr.
@BrianBi связанный с вами вопрос записывается в указатель с помощью std::memory_order_relaxed, этот вопрос записывается с помощью std::memory_order_release. Они похожи, так что спасибо за ссылку.
Хорошая мысль, хотя это ничего не меняет, потому что memory_order_release
не имеет эффекта, если нет соответствия memory_order_acquire
или сильнее (я собираюсь притвориться, что memory_order_consume
не существует)
@BrianBi «memory_order_release не действует, если нет соответствующего memory_order_acquire [...]», вы уверены? У вас есть цитата или что-то еще? Мне это кажется большой претензией.
@nmr в этом есть зависимость вновь выделенной памяти. Это само по себе имеет определенные последствия для памяти.
@BrianBi, который release
требует обработки acquire
, чтобы что-то сделать, - это неправильно. (1) Операция делает определенные вещи и имеет последствия даже без соответствия acquire
. (2) это больше модель памяти C++, на практике операция может фактически не делать ничего, кроме атомарной расслабленной операции на некоторых архитектурах.
@ ALX23z: На практике компиляторы C++ реализуют release
одинаково независимо от контекста и без проверки всей программы на наличие возможного средства чтения как минимум с порядком consume
. Но поскольку стандарт ISO C++ ничего не гарантирует, нет, никаких гарантий нет. Раньше ничего не происходит, поэтому нет гарантированного порядка в абстрактной машине С++. Только в некоторых, но не во всех конкретных реализациях на ассемблере для определенных машин реальными компиляторами, которые решили не искать этот UB и специально ломать код.
@ ALX23z Похоже, вы говорите, что операция с семантикой выпуска по-прежнему имеет эффект, даже если нет соответствующего приобретения. Это правда, конечно. Я имел в виду, что нет никакой разницы между выпуском и расслаблением, если нет соответствующего приобретения, поэтому нет существенной разницы между этим вопросом и другим, который я связал.
@BrianBi нет, даже без сопоставления получения операция выпуска выполняет другую операцию, чем операция расслабления. Что именно происходит, во многом зависит от аппаратного обеспечения. Но даже не вникая в аппаратные нюансы, добавляет различные ограничения на оптимизации компилятора.
@ ALX23z Я говорю только с точки зрения того, что гарантирует стандарт C++.
@BrianBi даже с точки зрения release
накладывает другие ограничения, чем relaxed
. Возможно, эти ограничения не очень полезны, если не вызывается acquire
, но они влияют на то, как компилятор может скомпилировать код.
@ ALX23z ALX23z Если вы считаете, что есть разница, объясните, в чем она заключается, и сошлитесь на стандарт.
@BrianBi говорит, что это влияет на ограничения операций с памятью. С расслабленным компилятором, свободным для переупорядочения операций, скажем, у вас есть op(); x.store(1,relaxed);
пост-оптимизация, которая может стать x.store(1,relaxed); op();
, это невозможно с операцией выпуска. Все записи op()
должны быть запланированы до магазина x
.
@ ALX23z И как бы вы заметили, произошло ли такое изменение порядка? Вы могли бы наблюдать это, читая из x в другом потоке. И если это чтение расслаблено, то оно может не увидеть результатов op()
, даже если магазин был релизным.
@BrianBi это может повлиять на производительность или вызвать сбой кода. Скажем, op()
занимает много времени, и какой-то другой поток тестирует его с ослабленными проверками атомарной переменной. Как только обнаруживается, что переменная верна, она начинает выполнять что-то еще. В такой ситуации переупорядочивание приведет к сбоям в работе кода/работе не так, как предполагалось.
@ ALX23z Это также может произойти с магазином релизов. Не гарантируется, что побочные эффекты op()
будут видны потоку, выполняющему расслабленное чтение, что может привести к потенциальному состоянию гонки.
@BrianBi нет, это не может произойти с release
порядком памяти, он запрещает любую операцию сохранения из op()
после выпуска. Я не говорю о том, видно ли то, что делает op()
— это не имеет значения. Я говорю о том, когда поток запланировал операцию. При чтении в расслабленном состоянии поток узнает, что если он выполнит чтение с получением, то операция op()
будет видна. Иногда видимость изменений не нужна, достаточно знать, что они готовы.
Порядок операций @BrianBi в потоке может быть важен при работе с атомарными элементами. В этом весь смысл consume
того, что он действует как забор для чтения потока, в отличие от расслабленного, при этом не фиксируя и не обновляя ничего, что делает выпуск и получение.
@ ALX23z Стандарт ничего не гарантирует в отношении порядка выполнения операций, если только такой порядок не будет наблюдаться. Похоже, вы согласны с тем, что порядок нельзя наблюдать, если нет соответствующей операции получения.
@BrianBi наблюдаемый и видимый — это две разные вещи. Видимый относится к другим потокам, которые видят изменения. Здесь изменение порядка операций можно наблюдать в любом случае, но это разрешено правилами порядка памяти при использовании ослабленной операции.
@ ALX23z Как именно, по вашему мнению, вы можете наблюдать за порядком?
@BrianBi, другой поток, который проверил переменную, заметил, что она верна, начал выполнять тяжелые вычисления. А в диспетчере задач можно заметить больше ядер, получающих 100%. Это довольно заметно, нет?
@ ALX23z ALX23z Это не считается наблюдаемым эффектом в стандарте. Выполнение ввода-вывода будет наблюдаемым эффектом, и если op()
также выполняет ввод-вывод, он может быть переупорядочен после выпуска хранилища.
@BrianBi изменение атомарной переменной всегда является наблюдаемым поведением с побочными эффектами. Это был очевидный пример того, почему это очень важно. Обычно оптимизация или переупорядочивание любых таких операций запрещены, как в случае с volatile, однако правила упорядочивания памяти разрешают это. По этой причине был придуман термин «видимый».
@ ALX23z Это побочный эффект, но его нельзя наблюдать. Ввод/вывод, напротив, можно наблюдать. Если один поток выполняет ввод-вывод, а затем освобождает хранилище, а другой поток считывает сохраненное значение через ослабленную загрузку, а затем выполняет ввод-вывод, две операции ввода-вывода могут выполняться в любом порядке.
@BrianBi, если другой поток прочитал часть данных, как это не наблюдаемое поведение? Это похоже на то, что вы пытаетесь сделать вид, что вся память C++ не является частью стандарта и недоступна для наблюдения.
@ ALX23z Боюсь, что сейчас мы спорим об определениях. Я хочу сказать, что, опять же, если поток 1 выполняет ввод-вывод, за которым следует освобождение хранилища в x
, а поток 2 выполняет расслабленную загрузку x
, которая считывает сохраненное значение, а затем выполняет ввод-вывод, могут произойти две операции ввода-вывода. в любом порядке. Если вы с этим согласны, значит я прав, что магазин релизов не мешает переупорядочивать. Если вы не согласны, то объясните, почему.
Нет, вы совсем сменили тему и заговорили о другом. Утверждение состоит в том, что существует разница между операцией освобождения и операцией расслабления, даже если нет соответствующего захвата. Буквально это и вызвало спор о том, что операция освобождения без приобретения не имеет значения.
@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 Это утверждение может запускаться как есть. Разницы не будет, если релиз поменять на расслабленный.
с инструкцией по выпуску. Релиз требует, чтобы сохранение x происходило после сохранения y. Условие if предполагает, что загрузка x происходит после загрузки y.
@ ALX23z Компилятору разрешено переупорядочивать хранилища, поскольку нет связи синхронизации с другим потоком. Следовательно, утверждение может сработать. Хранилище релиза не предотвращает такое переупорядочивание, ЕСЛИ нет соответствующей загрузки для получения. Пожалуйста, прочтите стандарт, если не верите мне.
@ ALX23z То, что ни один из известных нам компиляторов не может сломать ваш пример кода и заставить его утверждать, не означает, что такой компилятор не может быть 1) соответствующим стандарту 2) программируемым (не слишком сложно сделать) 3) полезным в реальном мире. Мы даже не говорим об абстрактных академических упражнениях. Первые поколения компиляторов рассматривали атомарные операции как непрозрачные забавные вызовы, выполняя нулевое преобразование вокруг них, но теперь они выполняют множество преобразований кода.
Что касается стандарта ISO C++, гарантирующего что-либо в абстрактной машине C++, нет, нет никакой гарантии, потому что ничто не создает отношения «происходит до» между инициализацией int
и чтением в другом потоке.
Но на практике компиляторы C++ реализуют release
одинаково независимо от контекста и без проверки всей программы на наличие возможного считывателя как минимум с порядком consume
.
В некоторых, но не во всех конкретных реализациях на ассемблере для реальных машин настоящими компиляторами это будет работать. (Потому что они решили не искать этот UB и намеренно ломать код с причудливым межпотоковым анализом единственно возможных операций чтения и записи этой атомарной переменной.)
DEC Alpha может классно взломать этот код, не гарантируя порядок зависимостей в ассемблере, поэтому нужны барьеры для memory_order_consume
, в отличие от всех(?) других ISA.
Учитывая текущее устаревшее состояние consume
, единственный способ получить эффективный ассемблер на ISA с порядком зависимостей (не Alpha), но которые не получают бесплатно (x86), — это написать такой код. Ядро Linux делает это на практике для таких вещей, как RCU.
Это требует, чтобы он был достаточно простым, чтобы компиляторы не могли нарушить порядок зависимостей, например. доказывая, что любой не-NULL read_ptr
будет иметь определенное значение, например адрес статической переменной.
Смотрите также
Мое единственное сожаление в жизни состоит в том, что я могу проголосовать за это только один раз.
@nmr: Если вы действительно хотите проголосовать больше, посмотрите мой ответ на связанных вопросах и ответах, в том числе Порядок использования памяти в C11 , в котором есть цитата Линуса Торвальдса о том, какие альфа-модели могут фактически нарушать причинно-следственную связь, как в статье. Спецификация разрешена (только несколько, и то редко, что делает высокую стоимость необходимых барьеров еще более болезненной). …
Я не уверен, в чем вопрос? данные (указатель), получаемые от
atomic
, являются потокобезопасными. и вы получаете доступ к 2 разным указателям. (одна из них по умолчанию построена (неинициализирована) атомарно)