Ответ
Такая ситуация называется состоянием гонки (Data Race) и является неопределённым поведением (Undefined Behavior) согласно стандарту C++. Программа может вести себя непредсказуемо.
Возможные последствия:
- Повреждение данных: Результат операции может быть некорректным.
- Критические сбои: Повреждение структур данных кучи (heap corruption), ведущее к падению программы.
- Несогласованное состояние: Разные потоки увидят разные значения переменной.
- Проблемы с оптимизацией: Компилятор вправе переупорядочивать операции с памятью в отсутствие синхронизации, что может привести к контринтуитивным результатам.
Пример гонки данных:
#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), то есть результат — хуй с горы, что получится, то и получится.
Что может случиться, если так делать?
- Данные поедут. Значение переменной превратится в какую-то дичь, потому что операции накладываются друг на друга.
- Всё накроется медным тазом. Можно так накосячить с выделением памяти, что программа просто рухнет с ошибкой доступа.
- Каждый поток увидит своё кино. Один считает, что значение одно, другой — совсем другое. Полный бардак.
- Компилятор тебя обманет. Он же не видит, что у тебя тут многопоточка, и начнёт переставлять операции местами для оптимизации. А ты потом сиди и гадай, почему в отладочной сборке работало, а в релизной — пиздец.
Вот тебе наглядный пример, как это выглядит:
#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): Самый простой способ — не делить данные вообще. У каждого потока своя копия, и нет проблем.
Главное правило, которое надо выжечь в мозгу: Если к объекту можно дотянуться из больше чем одного потока, и кто-то его может менять — ебушки-воробушки, защищай доступ! Либо мьютексом, либо атомиком, либо другой синхронизацией. Иначе будет тебе хиросима в коде, а не программа.