Все ли потоки имеют доступ к глобальной переменной?

Ответ

Да, все потоки, созданные в рамках одного процесса, имеют доступ к глобальным (статическим) переменным, так как они находятся в общем адресном пространстве. Однако такой несинхронизированный доступ приводит к состоянию гонки (race condition). Для безопасной работы с общими данными необходимо использовать механизмы синхронизации.

Основные подходы в C++:

  1. Мьютексы (std::mutex): Гарантируют эксклюзивный доступ.
  2. Атомарные операции (std::atomic): Позволяют выполнять операции чтения-записи без блокировок для примитивных типов.
  3. Условные переменные (std::condition_variable): Для координации потоков по событиям.

Пример с std::mutex и RAII-оберткой std::lock_guard:

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

int global_counter = 0;
std::mutex global_counter_mutex;

void increment_global(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        // Автоматически блокирует и разблокирует мьютекс
        std::lock_guard<std::mutex> lock(global_counter_mutex);
        ++global_counter; // Критическая секция защищена
    }
}

int main() {
    std::thread t1(increment_global, 100000);
    std::thread t2(increment_global, 100000);

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

    // Без мьютекса результат был бы меньше 200000
    std::cout << "Final counter value: " << global_counter << std::endl; // 200000
    return 0;
}

Почему это важно: Без синхронизации операция ++global_counter (которая состоит из чтения, изменения и записи) может быть прервана другим потоком, что ведет к потере обновлений и неопределенному поведению программы.

Ответ 18+ 🔞

Да ты представь, сидишь ты такой, пишешь код, и тут тебе в голову приходит гениальная идея — запустить несколько потоков, чтобы всё быстрее считалось. И вроде всё логично: потоки в одном процессе, значит, все глобальные переменные — это как общий холодильник на всю коммуналку. Захотел — взял, захотел — положил.

А потом начинается ёперный театр. Один поток только прочитал значение, второй уже своё туда впендюрил, первый обратно пишет по старой памяти — и всё, приехали. Состояние гонки, блядь. Результат получается такой, что хоть волком вой. Один говорит — 150 тысяч, другой — 180, а по факту нихуя не сходится. Удивление пиздец, честно.

Вот смотри, в чём суть. Чтобы не было этой пиздопроебибны, нужно эти общие данные как-то контролировать. В C++ для этого есть несколько штук, без которых — просто манда с ушами.

Первое — мьютексы (std::mutex). Это как ключ от сортира на вокзале. Пока один держит — остальные в очереди стоят и бздят. Но чтобы не забыть ключ в замке (разблокировать), умные люди придумали RAII-обёртки. std::lock_guard — это твой верный оруженосец. Вошёл в критическую секцию — он мьютекс взял, вышел — сам отпустил. Красота.

Второе — атомарные операции (std::atomic). Это для тех, кому мьютексы — как костыли для чемпиона по бегу. Для простых типов (int, bool) можно делать операции чтения-записи так, чтобы они были неразрывными. Без всей этой возни с блокировками. Быстро и чётко.

Третье — условные переменные (std::condition_variable). Это когда потоки должны не просто данные делить, а ещё и друг друга ждать. Типа «я сделал свою часть, сплю, разбуди, когда ты закончишь». Сложнее, но иногда без них — никуда.

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

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

int global_counter = 0; // Наш общий «холодильник»
std::mutex global_counter_mutex; // Ключ от этого холодильника

void increment_global(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        // Сейчас войдёт lock_guard и скажет: "Стоять! Разрешение есть только у меня!"
        std::lock_guard<std::mutex> lock(global_counter_mutex);
        ++global_counter; // Вот эта строчка — священная корова. К ней только по одному.
    }
}

int main() {
    // Запускаем двух работяг
    std::thread t1(increment_global, 100000);
    std::thread t2(increment_global, 100000);

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

    // А теперь смотрим, не обосрались ли они в общий счётчик
    std::cout << "Final counter value: " << global_counter << std::endl; // Должно быть 200000
    return 0;
}

А без мьютекса что? Операция ++global_counter — она же не атомарная, ёпта! Она ж три шага: прочитать значение, увеличить его на единицу, записать обратно. Пока один поток это делает, второй может влезть со своим чтением, и всё, пиши пропало. Значения потеряются, программа будет вести себя как хитрая жопа — вроде работает, а результат ебать копать какой-то левый.

Так что, чувак, доверия к потокам — ноль ебать. Либо блокировки, либо атомики. Иначе будет тебе не многопоточность, а гомосеки налетели, и все лезут в одну дырку одновременно.