Состояние гонки при увеличении и уменьшении глобальной переменной в C++

Я нашел пример состояния гонки, которое мне удалось воспроизвести под g++ в Linux. Чего я не понимаю, так это того, как порядок операций имеет значение в этом примере.

int va = 0;

void fa() {
    for (int i = 0; i < 10000; ++i)
        ++va;
}

void fb() {
    for (int i = 0; i < 10000; ++i)
        --va;
}

int main() {
    std::thread a(fa);
    std::thread b(fb);
    a.join();
    b.join();
    std::cout << va;
}

Я могу понять, что порядок имеет значение, если бы я использовал va = va + 1;, потому что тогда RHS va мог бы измениться, прежде чем вернуться к назначенному LHS va. Может кто-нибудь прояснить?


person sornbro    schedule 04.09.2019    source источник
comment
Не уверен, что нуждается в уточнении. Вы сами говорите, что это гонка. Расы вводят неопределенное поведение. Все может случиться.   -  person SergeyA    schedule 04.09.2019
comment
Оба потока пытаются изменить va одновременно, даже внутри циклов for. Кто выигрывает? Неизвестно, поэтому окончательное значение va может быть любым.   -  person Ripi2    schedule 04.09.2019
comment
Я подозреваю, что вы считаете операции увеличения и уменьшения числа атомарными. Они не.   -  person molbdnilo    schedule 04.09.2019
comment
Даже если операция является атомарной, без синхронизации нет никакой гарантии, что изменения в va действительно будут перемещены в место, где их смогут увидеть другие потоки. Например, va вполне может храниться в регистре.   -  person François Andrieux    schedule 04.09.2019
comment
Кстати, оптимизатору разрешено изменять эти методы как va += 10000;, va += 10000;, что еще больше снижает шансы увидеть эффект гонки.   -  person Jarod42    schedule 04.09.2019
comment
Обратите внимание, что гонка данных — это статическое свойство программы. Это не поведение.   -  person molbdnilo    schedule 04.09.2019
comment
Обратите внимание, что общий порядок также имеет значение, если вы правильно синхронизируете доступ. Предположим, что один поток будет выполнять va = 0;, а другой va = 1;, тогда даже в правильном коде (без гонки данных) вы не обязательно будете знать, какой поток будет первым.   -  person 463035818_is_not_a_number    schedule 04.09.2019


Ответы (3)


Стандарт говорит (цитируя последний проект):

[введение.гонки]

Два вычисления выражения конфликтуют, если одно из них изменяет ячейку памяти ([intro.memory]), а другая читает или изменяет ту же ячейку памяти.

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

В вашей примерной программе есть гонка данных, и поведение программы не определено.


Чего я не понимаю, так это того, как порядок операций имеет значение в этом примере.

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

можно понять, что порядок имеет значение, если бы я использовал va = va + 1; потому что тогда RHS va могла измениться, прежде чем вернуться к назначенному LHS va

То же самое относится и к оператору приращения. Абстрактная машина будет:

  • Чтение значения из памяти
  • Увеличить значение
  • Записать значение обратно в память

Там есть несколько шагов, которые могут чередоваться с операциями в другом потоке.

Даже если бы в каждом потоке была одна операция, не было бы гарантии четко определенного поведения, если бы эти операции не были атомарными.


Примечание, выходящее за рамки C++: ЦП может иметь одну инструкцию для увеличения целого числа в памяти. Например, у x86 есть такая инструкция. Его можно вызывать как атомарно, так и неатомарно. Для компилятора было бы расточительно использовать атомарную инструкцию, если вы явно не используете атомарные операции в C++.

person eerorika    schedule 04.09.2019

Важной идеей здесь является то, что при компиляции C++ он "переводится" на язык ассемблера. Преобразование ++va или --va приведет к ассемблерному коду, который перемещает значение va в регистр, а затем сохраняет результат прибавления 1 к этому регистру обратно к va в отдельной инструкции. Таким образом, это точно так же, как va = va + 1;. Это также означает, что операция va++ не обязательно является атомарной.

См. здесь объяснение как будет выглядеть ассемблерный код для этих инструкций.

Чтобы выполнять атомарные операции, переменная может использовать механизм блокировки. Вы можете сделать это, объявив атомарную переменную (которая будет обрабатывать синхронизацию потоков за вас):

std::atomic<int> va;

Ссылка: https://en.cppreference.com/w/cpp/atomic/atomic< /а>

person fendall    schedule 04.09.2019
comment
imho ваша первая часть немного вводит в заблуждение. UB может привести к сборке, которая выглядит хорошо сегодня, но не завтра - person 463035818_is_not_a_number; 04.09.2019
comment
@formerlyknownas_463035818 Я попытался сделать это более ясным, подчеркнув, что операция С++ не является атомарной, поскольку базовые инструкции сборки выполняют получение и установку отдельно. - person fendall; 04.09.2019
comment
на самом деле только сейчас я понимаю, что вы говорите ;) +1 - person 463035818_is_not_a_number; 04.09.2019

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

При этом, если вы хотите понять, что на самом деле делает ваш компьютер, когда эта программа запущена, может помочь предположить, что ++va совпадает с va = va + 1. На самом деле стандарт говорит, что они идентичны, и компилятор, скорее всего, скомпилирует их одинаково. Поскольку ваша программа содержит UB, компилятору не требуется делать ничего разумного, например, использовать инструкцию атомарного приращения. Если вам нужна инструкция атомарного приращения, вы должны были сделать va атомарной. Точно так же --va совпадает с va = va - 1. Так что на практике возможны разные результаты.

person Brian Bi    schedule 04.09.2019