Очереди и фоновые задачи на C++: пишем асинхронность без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему твоя программа тормозит, и как это исправить

Представь: ты написал крутой парсер на C++, который должен обработать 10 000 URL-адресов. Ты запускаешь его, идёшь за кофе, возвращаешься — а он всё ещё грузит первый сайт. Знакомо? Проблема в том, что программа работает синхронно: пока один запрос выполняется, остальные ждут своей очереди, как в советском универмаге.

В современном мире пользователи не прощают задержек. Если сервер отвечает дольше 200 мс — ты теряешь клиентов. Но как заставить C++ работать быстрее, не переписывая всё с нуля? Ответ — очереди и фоновые задачи. Это не просто модный термин, а фундаментальный паттерн, который используют в ядре Linux, игровых движках и высоконагруженных сервисах.

В этой статье мы разберёмся, как устроены очереди задач на C++, напишем свой простой thread pool и научимся обрабатывать данные в фоне без гонок данных и дедлоков. Готов прокачать свой код? Поехали.

Что такое очередь задач и зачем она нужна

Очередь задач (task queue) — это структура данных, которая работает по принципу FIFO (First In, First Out). Представь себе конвейер: ты добавляешь задачи в один конец, а потоки-исполнители забирают их с другого. Это основа паттерна Producer-Consumer (производитель-потребитель).

Зачем это нужно в C++? Вот три главные причины:

  • Параллельная обработка — несколько потоков одновременно берут задачи из очереди, ускоряя работу в разы.
  • Асинхронность — основной поток не блокируется, а продолжает принимать новые запросы или обновлять интерфейс.
  • Балансировка нагрузки — если одна задача тяжёлая, другие потоки подхватят лёгкие, и система не зависнет.

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

Пишем простую очередь на C++ с нуля

Начнём с базового примера. В стандартной библиотеке C++ есть std::queue, но для многопоточности её нужно обернуть в защищённый класс. Вот минимальная реализация потокобезопасной очереди:

#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <thread>

class ThreadSafeQueue {
private:
    std::queue<std::function<void()>> tasks;
    std::mutex mtx;
    std::condition_variable cv;

public:
    void push(std::function<void()> task) {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(task);
        cv.notify_one();  // Будим один ожидающий поток
    }

    std::function<void()> pop() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { return !tasks.empty(); });
        auto task = tasks.front();
        tasks.pop();
        return task;
    }
};

Здесь мы используем std::mutex для защиты доступа к очереди и std::condition_variable, чтобы потоки не тратили процессор впустую, ожидая новые задачи. Когда задача появляется, мы будим один поток с помощью notify_one().

Но это только половина дела. Нам нужен пул потоков (thread pool), который будет выполнять эти задачи.

Создаём пул потоков: сердце фоновых задач

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

class ThreadPool {
private:
    std::vector<std::thread> workers;
    ThreadSafeQueue queue;
    bool stop = false;

    void worker_loop() {
        while (!stop) {
            auto task = queue.pop();
            if (task) task();
        }
    }

public:
    ThreadPool(size_t num_threads) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back(&ThreadPool::worker_loop, this);
        }
    }

    void enqueue(std::function<void()> task) {
        queue.push(task);
    }

    ~ThreadPool() {
        stop = true;
        // Добавляем фиктивные задачи, чтобы разбудить все потоки
        for (size_t i = 0; i < workers.size(); ++i) {
            queue.push([] {});
        }
        for (auto& worker : workers) {
            if (worker.joinable()) worker.join();
        }
    }
};

Обрати внимание на деструктор: мы добавляем пустые задачи, чтобы каждый поток вышел из pop() и завершился. Это элегантный способ завершить работу без дедлоков.

Теперь мы можем запускать фоновые задачи просто:

int main() {
    ThreadPool pool(4);  // 4 потока

    for (int i = 0; i < 10; ++i) {
        pool.enqueue([i] {
            std::cout << "Task " << i << " executed by thread "
                      << std::this_thread::get_id() << std::endl;
        });
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

Запустив этот код, ты увидишь, как задачи распределяются между потоками. Красота, правда?

Как обрабатывать результаты фоновых задач

Просто запустить задачу в фоне — мало. Часто нужно получить результат. Для этого используем std::future и std::promise. Давай модернизируем наш пул, чтобы он возвращал результаты:

#include <future>

template<typename F, typename... Args>
auto enqueue(F&& f, Args&&... args) 
    -> std::future<decltype(f(args...))> 
{
    using return_type = decltype(f(args...));
    auto task = std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );
    std::future<return_type> result = task->get_future();
    queue.push([task]() { (*task)(); });
    return result;
}

Теперь мы можем делать так:

auto future = pool.enqueue([](int a, int b) { 
    return a + b; 
}, 5, 3);

int sum = future.get();  // Блокируемся, пока не получим результат
std::cout << "Sum: " << sum << std::endl;

Это даёт гибкость: ты можешь запустить несколько задач, а потом собрать результаты в любом порядке.

Реальный пример: парсинг веб-страниц в фоне

Допустим

Блоги

Рекомендаций по книгам