Параллельная запись одного и того же значения

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

std::vector<int> vec(32, 1); // Initialize vec with 32 times 1
std::vector<std::thread> threads;

for (int i = 0 ; i < 8 ; ++i) {
    threads.emplace_back([&vec]() {
        for (std::size_t j = 0 ; j < vec.size() ; ++j) {
            vec[j] = 0;
        }
    });
}

for (auto& thrd: threads) {
    thrd.join();
}

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

Если существует потенциально опасная гонка данных, будет ли достаточно использовать хранилище std::vector<std::atomic<int>> вместо std::memory_order_relaxed, чтобы предотвратить гонку данных?


person Morwenn    schedule 13.04.2014    source источник
comment
На самом деле довольно легко определить, является ли что-то гонкой данных-UB или нет: если одновременно может выполняться более одной записи, но не чтения, у вас проблемы. Если более одного чтения, но записи не происходит, все в порядке. Если одна запись и хотя бы одно чтение происходят одновременно, вы снова облажались. Кратко: (›1 запись) ИЛИ (запись+чтение) — проблема.   -  person stefan    schedule 13.04.2014
comment
Используйте атомы, и вы в безопасности. Не вижу смысла вводить здесь УБ.   -  person usr    schedule 13.04.2014
comment
связанные   -  person user28667    schedule 22.07.2020


Ответы (3)


Ответ языкового юриста, [intro.multithread] n3485

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

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


будет ли достаточно использовать std::vector<std::atomic<int>> вместо std::memory_order_relaxed для предотвращения гонки данных?

Да. Эти обращения являются атомарными, и существует отношение происходит до, вводимое через объединение потоков. Любое последующее чтение из потока, порождающего этих рабочих процессов (синхронизированных через .join), является безопасным и определенным.

person dyp    schedule 13.04.2014

Это гонка данных, и компиляторы в конечном итоге станут достаточно умными, чтобы неправильно скомпилировать код, если они еще этого не сделали. См. раздел как неправильно скомпилировать программы с "безвредными" гонками данных раздел 2.4 о том, почему запись одного и того же значения нарушает код.

person nwp    schedule 14.04.2014
comment
Это было задумано как комментарий к ответу Language-lawyer от dyp, чтобы показать, что это действительно имеет последствия, но мне не хватает необходимой репутации. - person nwp; 14.04.2014

Подробный ответ о реализации:

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

Почему? Аппаратное обеспечение упорядочивает доступ к одной и той же ячейке памяти. Единственное, что может пойти не так, это когда несколько ячеек памяти записываются одновременно, потому что тогда у вас нет аппаратной гарантии того, что доступ к нескольким ячейкам последовательно упорядочен одинаково. Например, если один процесс записывает 0x0000000000000000, а другой записывает 0xffffffffffffffff, ваше оборудование может решить по-разному упорядочить доступ к разным байтам, что приведет к чему-то вроде 0x00000000ffffffff.

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

Современное оборудование не обрабатывает доступ к памяти побайтно, вместо этого процессоры взаимодействуют с основной памятью с помощью строк кэша, а ядра обычно могут обмениваться данными со своими кэшами с помощью 8-байтовых слов. Таким образом, установка правильно выровненного указателя является атомарной операцией, на которую можно положиться при реализации алгоритмов без блокировки. Это использовалось в ядре Linux до того, как стали доступны более мощные атомарные операции. C++ формализует это в виде типов atomic<>, добавляя поддержку аппаратных функций более высокого уровня, таких как запись после чтения, атомарные приращения и тому подобное.

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


@Downvoters:

Вопрос не помечен как [language-lawyer], а в ответе явно указано "Подробный ответ на вопрос о реализации". Было намеренно объяснить, как будет выглядеть УБ в программе в реальной жизни. Этот ответ был написан, чтобы дополнить принятый ответ (у которого есть мой положительный отзыв) другим взглядом на вопрос.

person cmaster - reinstate monica    schedule 13.04.2014
comment
Вы также не можете полагаться на то, что компилятор не будет использовать UB. Цикл по сути представляет собой memzero. Кто знает, что компилятор делает с этим? - person usr; 13.04.2014
comment
@usr Ну, на оптимизатор обычно можно положиться, чтобы он не разбирался в проблемах параллелизма. Это сделало бы оптимизацию слишком сложной, и единственным эффектом было бы нарушение работы многопоточного кода. И в цикле нет поведения UB, когда вы предполагаете, что есть только один поток. - person cmaster - reinstate monica; 13.04.2014
comment
Почему? Аппаратное обеспечение упорядочивает доступ к одной и той же ячейке памяти. У вас есть оракул, который знает все аппаратные средства, на которых когда-либо будет работать программа OP? - person Casey; 13.04.2014
comment
@Casey Нет, не знаю, но я знаю, как байты передаются в основную память; и я даже имею некоторое представление о том, что аппаратно возможно, а что нет. Следовательно, я совершенно уверен, что программа OP никогда не будет работать на оборудовании, где две одновременные записи одних и тех же данных приводят к неопределенному поведению. Но, конечно, это подробный ответ на реализацию :-) - person cmaster - reinstate monica; 13.04.2014