Выполняется ли оператор в C++, то есть инструкции, заканчивающиеся точкой с запятой ';', целиком в многопоточном контексте?

Скажем, у меня есть std::atomic с именем num:

std::atomic<double> num{3.1415};

поскольку С++ не полностью поддерживает арифметические операции для атомов, за исключением «целочисленных типов», я не могу сделать:

num *= 10;

Вместо этого мне придется сделать

num.store( num.load() * 10 );  

Есть ли риск гонки данных? например, после num.load(), но до num.store(), возможно ли, что другой поток уже обновил атомарный код?

Состояние гонки означает, что два потока получают доступ к одной и той же переменной или изменяют ее без упорядочивания (например, поэтому одно обновление может быть прервано другим при частичном завершении). Возможно, что num будет обновлено после вызова num.load() и перед вызовом num.store() (умножение не является атомарной операцией), но это не является состоянием гонки, поскольку обновления num будут упорядочены. В результате обновление будет потеряно (num перезаписано) num.store().

Peter 18.07.2024 04:53

@Питер: позвольте мне не согласиться... то, что хранилище и загрузка являются атомарными и упорядоченными, не означает, что нет состояния гонки. Относительное время доступа для чтения/записи из других потоков может повлиять на логическую корректность использования (обновления и значения, считанные из) num: это приводит к состоянию гонки. Точнее, если другой поток обновляет num после того, как поток «*=10» выполнил загрузку и до того, как он выполнил сохранение, то обновление другого потока затирается/перезаписывается. Например, два потока, пытающиеся обновить *=10, могут в конечном итоге увеличить num только в 10 раз, а не в 100.

Tony Delroy 18.07.2024 06:42

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

Peter Cordes 18.07.2024 07:18

@TonyDelroy, Когда вы разговариваете с людьми, занимающимися C++, «состояние гонки» означает нечто иное, чем то, что оно означает для остального мира. В C++ понятия «состояние гонки» и «гонка данных» означают одно и то же, а в приведенном здесь примере их нет. Эта программа имеет только два возможных результата, оба из которых четко определены. Конечным значением переменной будет то, которое было сохранено тем или иным потоком. Даже если вы не можете знать, что именно, люди в C++ не говорят «состояние гонки». Они говорят это только тогда, когда программа нарушает определенные правила таким образом, что результат становится совершенно неопределенным.

Solomon Slow 18.07.2024 13:21

@SolomonSlow: «когда вы разговариваете с людьми, работающими на C++» — возможно, с теми, с кем вы разговаривали, но не с теми, с кем я разговаривал. Я согласен с точкой зрения Питера Кордеса, хотя и противопоставляю специфическую для C++ терминологию «гонки данных» общему понятию «состояния гонки» для разработчиков/потоков.

Tony Delroy 18.07.2024 17:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
157
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Есть ли риск гонки данных?

Да. На самом деле, причина, по которой вы не можете сделать это с *=, заключается в том, что это может заставить вас думать, что это не возможная гонка данных. Заставляя вас явно выполнять load и store, вы даете понять, что вы выполняете две операции.

Две последовательные атомарные операции не являются атомарными в целом.

std::atomic предоставляет compare_exchange_weak, которого достаточно для реализации fetch_mul или operator*, если они захотят. Просто неудобно, что заставляют катать свой. Или, говоря иначе, если бы *= существовал, это был бы атомный RMW. Итак, ваше первое предложение кажется странным. Возможно, они этого не предоставляют, потому что ни одно реальное оборудование не имеет атомарной формы с одной инструкцией, поэтому только машины LL/SC могут сделать это так просто, как для fetch_add/+=. И/или потому, что это требуется реже.
Peter Cordes 18.07.2024 07:15

Дело не в том, что это не будет «атомарный процессор с одной инструкцией»; дело в том, что это будет «довольно дорогой атомарный метод», требующий цикла, чтобы увидеть, не изменил ли кто-нибудь значение переменной, когда вы ее впервые прочитали.

Nicol Bolas 18.07.2024 15:42
fetch_xor, fetch_and и т. д. требуют цикла повтора CAS на x86, если вы используете возвращаемое значение. Только fetch_sub/fetch_add можно оптимизировать до lock xadd. Честно говоря, некоторые процессоры не имеют инструкции целочисленного умножения, а циклическая реализация умножения создает большее окно гонки, что потенциально увеличивает количество повторных попыток. Вероятно, это наиболее вероятная причина отсутствия operator*=/fetch_mul.
Peter Cordes 18.07.2024 21:05
Ответ принят как подходящий

Хотя num.store(num.load() * 10) все еще имеет состояние гонки, вы можете использовать atomic_compare_exchange , чтобы делать то, что хотите.

Общая идея использования операций сравнения и обмена (иногда называемых сравнением и обменом) заключается в том, что в псевдокоде

do {
     last-value-read = atomic-read-of-memory-location
     desired-value = last-value-read * 10
} while (atomic-compare-and-exchange(atomic-update-to=desired_value,
                                     only-if-memory-still=last-value-read));

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

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

Я хотел бы добавить к существующим ответам, что

Есть ли риск гонки данных?

Нет, не в соответствии со стандартным определением C++ «гонки данных»:

[intro.races]/21

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

Ваша программа эквивалентна:

auto val = num.load() * 10;
// here you could have some more statements
num.store(val);

Итак, со стандартной точки зрения у вас нет гонки данных и неопределенного поведения. Однако,

после num.load(), но до num.store(), возможно ли, что другой поток уже обновил атомарный код?

Абсолютно да. И вы не можете быть уверены, каким будет окончательное значение вашего num после запуска N потоков. Но это не Неопределённое поведение, вы просто написали программу, которая ведет себя недетерминированным образом.

«Гонка данных» — это не то же самое, что «состояние гонки». Первое — это особый вид UB, который может произойти при доступе к неатомарным переменным из разных потоков. Последнее является общим словом для обозначения ошибок многопоточности.

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

ОП не спрашивал, есть ли в программе гонка данных; он спросил, находится ли он «под угрозой» этого. И это так.

Nicol Bolas 18.07.2024 15:43

@NicolBolas «И это так». Но это не так? Гонка данных по определению требует наличия неатомарного объекта, но его нет. eel.is/c++draft/intro.races#def:data_race

HolyBlackCat 18.07.2024 15:50

Чтобы четко ответить на одну часть вопроса, которая является распространенным заблуждением:

Наличие нескольких операций в одном операторе не приводит к их атомарному выполнению.

Если бы это было так, вы бы смогли создавать сколь угодно сложные атомарные операции, включающие любое количество объектов (например, с помощью оператора запятой). Но настоящие машины не имеют возможности выполнить такую ​​задачу. Обычно они предоставляют очень ограниченный набор атомарных операций без блокировок, которые действуют только на один объект, а атомарные функции-члены чтения-изменения-записи std::atomic<T> предоставляют способ их использования.

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

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

PkDrew 24.07.2024 17:13

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