В чём разница между Thread pool и прямым порождением потоков (std::thread)?

«В чём разница между Thread pool и прямым порождением потоков (std::thread)?» — вопрос из категории Многопоточность, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

Ключевые отличия и практические последствия:

  1. Производительность и накладные расходы: Создание и уничтожение потока — дорогая операция (выделение памяти, настройка стека, взаимодействие с ОС). Пул потоков устраняет эти накладные расходы, переиспользуя существующие потоки.
  2. Управление ресурсами: При прямом создании легко породить чрезмерное количество потоков, что может исчерпать системные ресурсы и привести к thrashing (переключению контекста). Пул позволяет жёстко ограничить максимальное число одновременно работающих потоков.
  3. Удобство и контроль: Пул потоков абстрагирует управление жизненным циклом потоков. Разработчик просто отправляет задачи (например, функторы) в очередь. Пул сам распределяет их по свободным потокам.

Пример прямого создания потоков в C++ (потенциально неэффективно):

#include <thread>
#include <vector>

void process_task(int task_id) {
    // Выполнение задачи...
}

void run_tasks_directly(int num_tasks) {
    std::vector<std::thread> threads;
    threads.reserve(num_tasks);

    for (int i = 0; i < num_tasks; ++i) {
        // Для КАЖДОЙ задачи создаётся новый поток
        threads.emplace_back(process_task, i);
    }

    for (auto& t : threads) {
        t.join(); // Ожидание завершения всех потоков
    }
}

Пример использования пула потоков (C++17 с std::async и политикой запуска):

#include <future>
#include <vector>

void run_tasks_with_pool(int num_tasks) {
    std::vector<std::future<void>> futures;
    futures.reserve(num_tasks);

    for (int i = 0; i < num_tasks; ++i) {
        // Задачи отправляются в очередь пула потоков исполнения.
        // Пул управляется реализацией стандартной библиотеки.
        futures.push_back(std::async(std::launch::async, process_task, i));
    }

    // Дожидаемся результатов (блокируем до завершения всех задач)
    for (auto& fut : futures) {
        fut.get();
    }
}

Когда что использовать:

  • Пул потоков — идеален для большого количества коротких, однотипных задач (обработка запросов, параллельные вычисления).
  • Прямое создание std::thread — может быть оправдано для долгоживущих, изолированных задач с уникальными требованиями или когда необходим полный контроль над потоком (например, установка приоритета, привязка к CPU).