Пример использования std::mutex

Я написал этот кусок кода в качестве теста:

#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?

3 ответа

Решение

Условия гонки - неопределенное поведение

Вы не можете утверждать, что должно произойти, когда участвует гонка данных. Ваше утверждение о том, что должны быть какие-то видимые доказательства разрыва данных (т. Е. Конечный результат - 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 или же countervolatile; тогда ваша гонка данных должна стать очевидной.

Гонки данных - это неопределенное поведение, что означает, что любое выполнение программы является действительным, включая выполнение программы, которое происходит так, как вам нужно. В этом случае компилятор, вероятно, оптимизирует ваш цикл в counter += a и первый поток завершается до начала второго, поэтому они никогда не конфликтуют.

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