Я написал этот кусок кода в качестве теста:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
int main() {
auto a = std::thread{ inc, 100000 };
auto b = std::thread{ inc, 100000 };
a.join();
b.join();
std::cout << counter;
return 0;
}
counter
переменная глобальная и так, создавая 2 потока a
а также b
Я бы ожидал найти гонку данных. Выход 200000, а не случайное число. Зачем?
Этот код является фиксированной версией, которая использует mutex
так что глобальная переменная может быть доступна только один раз (1 поток за раз). Результат еще 200000.
std::mutex mutex;
auto inc(int a) {
mutex.lock();
for (int k = 0; k < a; ++k)
++counter;
mutex.unlock();
}
Факт таков. Мьютекс-решение дает мне 200000, что правильно, потому что только одна угроза за раз может получить доступ к счетчику. Но почему non-mutex-решение все еще показывает 200000?
Вы не можете делать заявления о том, что должен случается, когда участвует гонка данных. Ваше утверждение, что там должен быть некоторым видимым доказательством разрыва данных (то есть, конечный результат — 178592 или что-то в этом роде), является ложным, потому что нет никаких оснований ожидать какого-либо такого результата.
Следующий код
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
Может быть юридически оптимизирован в соответствии со стандартом C ++ в
auto inc(int a) {
counter += a;
}
Обратите внимание, как количество записей в counter
был оптимизирован с O(a)
в O(1)
, Это довольно существенно. Это означает, что возможно (и вероятно), что запись в counter
завершается еще до инициализации второго потока, что делает наблюдение за разрывом данных статистически маловероятным.
Если вы хотите, чтобы этот код вел себя так, как вы ожидаете, подумайте о маркировке переменной counter
как volatile
:
#include <iostream>
#include <thread>
#include <mutex>
volatile int counter = 0;
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
int main() {
auto a = std::thread{ inc, 100000 };
auto b = std::thread{ inc, 100000 };
a.join();
b.join();
std::cout << counter;
return 0;
}
Имейте в виду, что это все еще неопределенное поведение, и не следует полагаться на какой-либо производственный код! Однако этот код, скорее всего, будет повторять состояние гонки, которое вы пытаетесь вызвать.
Вы также можете попробовать большее число, чем 100000, поскольку на современном оборудовании, даже без оптимизации, цикл 100000 может быть довольно быстрым.
Проблема здесь в том, что ваша гонка данных чрезвычайно мала. Любой современный компилятор превратит ваш inc
функция к counter += a
, таким образом, окно гонки очень мало — я бы даже сказал, что, скорее всего, когда вы начнете второй поток, первый из них уже закончен.
Это не делает это менее неопределенным поведением, но объясняет результат, который вы видите. Вы можете сделать компилятор менее умным в отношении вашего цикла, например делая a
или же k
или же counter
volatile
; тогда ваша гонка данных должна стать очевидной.
Гонки данных — это неопределенное поведение, что означает, что любое выполнение программы является действительным, включая выполнение программы, которое происходит так, как вам нужно. В этом случае компилятор, вероятно, оптимизирует ваш цикл в counter += a
и первый поток завершается до начала второго, поэтому они никогда не конфликтуют.