Что произойдёт при одновременном обращении двух потоков к одной переменной в C++ без синхронизации?

Ответ

Такая ситуация называется состоянием гонки (Data Race) и является неопределённым поведением (Undefined Behavior) согласно стандарту C++. Программа может вести себя непредсказуемо.

Возможные последствия:

  1. Повреждение данных: Результат операции может быть некорректным.
  2. Критические сбои: Повреждение структур данных кучи (heap corruption), ведущее к падению программы.
  3. Несогласованное состояние: Разные потоки увидят разные значения переменной.
  4. Проблемы с оптимизацией: Компилятор вправе переупорядочивать операции с памятью в отсутствие синхронизации, что может привести к контринтуитивным результатам.

Пример гонки данных:

#include <iostream>
#include <thread>
#include <vector>

int shared_counter = 0; // Разделяемая переменная

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++shared_counter; // НЕАТОМАРНАЯ операция: чтение-изменение-запись
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    // Результат почти гарантированно будет меньше 200000
    std::cout << "Final counter value: " << shared_counter << std::endl;
    return 0;
}

Способы обеспечить безопасный доступ:

  • Мьютексы (std::mutex, std::shared_mutex): Гарантируют эксклюзивный доступ.
  • Атомарные операции (std::atomic<T>): Для простых типов данных. В примере выше нужно было использовать std::atomic<int> shared_counter{0};.
  • Lock-free структуры данных: Для высокопроизводительных сценариев.
  • Локальная память потока (thread_local): Если данные не нужно разделять.

Правило: Любой доступ (чтение или запись) к неатомарному объекту, который может быть изменён из другого потока, должен быть защищён механизмом синхронизации.

Ответ 18+ 🔞

А, ну это классика, ёпта! Ситуация, когда два потока лезут в одну переменную без спросу, называется состоянием гонки (Data Race). По стандарту C++ это неопределённое поведение (Undefined Behavior), то есть результат — хуй с горы, что получится, то и получится.

Что может случиться, если так делать?

  1. Данные поедут. Значение переменной превратится в какую-то дичь, потому что операции накладываются друг на друга.
  2. Всё накроется медным тазом. Можно так накосячить с выделением памяти, что программа просто рухнет с ошибкой доступа.
  3. Каждый поток увидит своё кино. Один считает, что значение одно, другой — совсем другое. Полный бардак.
  4. Компилятор тебя обманет. Он же не видит, что у тебя тут многопоточка, и начнёт переставлять операции местами для оптимизации. А ты потом сиди и гадай, почему в отладочной сборке работало, а в релизной — пиздец.

Вот тебе наглядный пример, как это выглядит:

#include <iostream>
#include <thread>
#include <vector>

int shared_counter = 0; // Вот эта переменная — наша общая беда

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++shared_counter; // ЭТО НЕ АТОМАРНО! Чтение, плюсанули, запись. И так в двух потоках сразу!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    // И тут мы все дружно охуеваем, потому что результат будет меньше 200000. Гонка, блядь!
    std::cout << "Final counter value: " << shared_counter << std::endl;
    return 0;
}

Как с этим бороться, чтобы не было мучительно больно?

  • Мьютексы (std::mutex): Классика жанра. Поставил замок — работай один, снял — пускай другой. Надёжно, но не всегда быстро.
  • Атомарные операции (std::atomic<T>): Для таких простых случаев, как счётчик — идеально. В примере выше надо было просто написать std::atomic<int> shared_counter{0}; и спать спокойно.
  • Lock-free алгоритмы: Для тех, кто хочет выжать максимум производительности и готов помучаться. Это уже высший пилотаж.
  • Локальные переменные потока (thread_local): Самый простой способ — не делить данные вообще. У каждого потока своя копия, и нет проблем.

Главное правило, которое надо выжечь в мозгу: Если к объекту можно дотянуться из больше чем одного потока, и кто-то его может менять — ебушки-воробушки, защищай доступ! Либо мьютексом, либо атомиком, либо другой синхронизацией. Иначе будет тебе хиросима в коде, а не программа.