Как можно обмениваться информацией между потоками в C++?

Ответ

В современном C++ есть несколько безопасных способов обмена данными между потоками:

  • Атомарные операции (std::atomic): Для простых типов данных (int, bool, указатели). Гарантируют отсутствие гонок данных без использования мьютексов.
    std::atomic<int> shared_counter{0};
    // Поток 1:
    shared_counter.fetch_add(1, std::memory_order_relaxed);
    // Поток 2:
    int value = shared_counter.load(std::memory_order_acquire);
  • Мьютексы и условные переменные (std::mutex, std::condition_variable): Для защиты критических секций и организации ожидания событий.
  • Очереди (std::queue + мьютекс или std::priority_queue): Классический шаблон "производитель-потребитель".
  • Каналы (std::promise/std::future): Для однократной передачи результата или исключения из одного потока в другой.
  • Потокобезопасные контейнеры: Если используется сторонняя библиотека вроде Intel TBB или самостоятельно написанные обертки.

Пример: потокобезопасная очередь для обмена сообщениями:

#include <queue>
#include <mutex>
#include <condition_variable>

template<typename T>
class ThreadSafeQueue {
    std::queue<T> queue_;
    mutable std::mutex mutex_;
    std::condition_variable cond_var_;
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mutex_);
        queue_.push(std::move(value));
        cond_var_.notify_one();
    }
    T pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        cond_var_.wait(lock, [this]{ return !queue_.empty(); });
        T value = std::move(queue_.front());
        queue_.pop();
        return value;
    }
};
// Использование: один поток push(), другой — pop().

Ответ 18+ 🔞

А, ну ты про это, про передачу данных между потоками! Да это же, блядь, вечная тема, как будто на дворе 2002-й год, а проблемы всё те же — как не наебаться с гонками данных и не получить неожиданный пиздец в три часа ночи. Слушай, тут вариантов, конечно, овердохуища, но главное — не выебываться и выбрать нормальный.

Вот смотри, самый простой и приятный способ для всяких счётчиков или флагов — это атомарные операции, std::atomic. Представь, что у тебя есть общая переменная, и к ней лезут сразу десять потоков. Без атомиков это гарантированный пиздец, данные превратятся в кашу. А так — всё чётко, как по маслу, без всяких мьютексов. Типа вот так:

std::atomic<int> shared_counter{0};
// Поток 1:
shared_counter.fetch_add(1, std::memory_order_relaxed);
// Поток 2:
int value = shared_counter.load(std::memory_order_acquire);

Видишь? Никакой возни, всё безопасно. Но это, конечно, для примитивов. Если тебе нужно что-то посерьёзнее передать — структуру какую-нибудь или сообщение — тут уже нужна тяжёлая артиллерия.

Классика жанра — мьютексы и условные переменные. Это как стоять в очереди в сортир на вечеринке: один зашёл — закрылся (захватил мьютекс), сделал дело — вышел (отпустил), следующий заходит. А условная переменная — это как стучать в дверь и орать: «Ты там скоро, ёпта? Я уже не могу!». Без них никуда, но код получается, блядь, на полстраницы, где половина — это работа с блокировками. Терпения ебать ноль, когда это пишешь.

Ещё есть каналы — std::promise и std::future. Это для разовой передачи, типа «сделай дело и верни мне результат». Удобно, но только на один раз. Если тебе нужно постоянно что-то кидать из потока в поток — это не вариант.

А теперь, внимание, самый жирный и правильный паттерн для такого обмена — потокобезопасная очередь. Это как почтовый ящик: один поток кидает в него письма (push), другой вынимает (pop). И всё это, блядь, обёрнуто в мьютекс и условную переменную, чтобы не было гонок и чтобы потребитель не тупил вхолостую. Вот смотри, как это примерно выглядит:

#include <queue>
#include <mutex>
#include <condition_variable>

template<typename T>
class ThreadSafeQueue {
    std::queue<T> queue_;
    mutable std::mutex mutex_;
    std::condition_variable cond_var_;
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mutex_);
        queue_.push(std::move(value));
        cond_var_.notify_one();
    }
    T pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        cond_var_.wait(lock, [this]{ return !queue_.empty(); });
        T value = std::move(queue_.front());
        queue_.pop();
        return value;
    }
};
// Использование: один поток push(), другой — pop().

Суть в чём? В push мы просто лочим мьютекс, пихаем значение в обычную очередь и будим один спящий поток через cond_var_.notify_one(). В pop — ждём, пока в очереди что-то появится (это делает cond_var_.wait с лямбдой), а потом забираем это что-то и возвращаем. Красота, ёпта! Никаких гонок, потребитель спит, пока нет работы, и просыпается ровно тогда, когда она появляется. Это и есть тот самый «производитель-потребитель», на котором держится половина многопоточных программ. Главное — не выёбывайся и не пытайся придумать велосипед, этот шаблон проверен годами и слезами.