← Назад к вопросам

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

2.0 Middle🔥 181 комментариев
#Многопоточность и синхронизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Это фундаментальная архитектурная разница, которая критична для производительности высоконагруженных систем.

Быстрое сравнение

АспектПрямое порождениеThread Pool
Затраты на созданиевысокие (каждый раз)низкие (переиспользование)
Завершение потокаявное удалениевозврат в пул
Управление памятьюO(n) для n потоковO(k) где k = fixed pool size
CPU Usageможет быть неэффективнооптимально
Context switchingвысокий (много потоков)контролируемый
Масштабируемостьплохаяхорошая
Сложностьпростаясложнее

Прямое порождение потоков (создание для каждой задачи)

// Наивный подход: создание нового потока для каждой задачи
class SimpleServer {
private:
    std::vector<std::thread> threads_;
public:
    void handleRequest(const Request& req) {
        // Создаём новый поток для каждого запроса
        threads_.emplace_back([this, req]() {
            processRequest(req);
        });
    }
    
    ~SimpleServer() {
        for (auto& thread : threads_) {
            if (thread.joinable()) {
                thread.join(); // ждём все потоки
            }
        }
    }
};

// Проблемы этого подхода:
int main() {
    SimpleServer server;
    
    for (int i = 0; i < 100000; i++) {
        Request req = createRequest(i);
        server.handleRequest(req); // создаёт 100k потоков! ❌
    }
}

Проблемы прямого создания потоков

1. Затраты на создание и удаление

// Создание потока занимает значительное время
std::chrono::high_resolution_clock::time_point start = 
    std::chrono::high_resolution_clock::now();

for (int i = 0; i < 1000; i++) {
    std::thread t([] { std::cout << "task\n"; });
    t.join(); // ждём завершения
}

auto duration = std::chrono::high_resolution_clock::now() - start;
// Может занять секунды или даже минуты для миллионов потоков!

На Linux создание потока:

  • Выделение стека (обычно 8 МБ)
  • Инициализация TLS (Thread Local Storage)
  • Регистрация в scheduler
  • Context switch
  • Очистка при завершении

2. Истощение ресурсов

// Если обслуживаем 100k одновременных соединений
for (int i = 0; i < 100000; i++) {
    // 100k * 8MB = 800GB памяти! ❌
    std::thread t(handleConnection);
}

// Система упадёт за недостатком памяти

3. Excessive context switching

// С тысячами потоков
std::vector<std::thread> threads;
for (int i = 0; i < 5000; i++) {
    threads.emplace_back([] {
        // Даже если потока мало работы, OS переключается между ними
        // Context switch стоит дорого (примерно 1-10 микросекунд)
        // С 5000 потоков на 4-core CPU:
        // каждый поток работает ~0.8ms перед переключением
        for (auto& t : threads) {
            t.join(); // ❌ очень неэффективно
        }
    });
}

4. Непредсказуемая производительность

// Нет контроля над параллелизмом
void handleRequests(const std::vector<Request>& requests) {
    for (const auto& req : requests) {
        std::thread t([req] {
            process(req); // может выполняться в 1, 10 или 1000 потоков
        });
    }
    // Система сама решает, сколько потоков запустить
    // Может быть очень неэффективно
}

Thread Pool: решение проблемы

Thread Pool — это пул переиспользуемых потоков, которые ждут задач.

class ThreadPool {
private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex task_mutex_;
    std::condition_variable cv_;
    bool shutdown_ = false;
public:
    ThreadPool(size_t num_threads) {
        // Создаём фиксированное количество потоков один раз
        for (size_t i = 0; i < num_threads; i++) {
            workers_.emplace_back([this] {
                while (true) {
                    std::unique_lock<std::mutex> lock(task_mutex_);
                    // Ждём либо задачи, либо shutdown сигнала
                    cv_.wait(lock, [this] { 
                        return !tasks_.empty() || shutdown_; 
                    });
                    
                    if (shutdown_ && tasks_.empty()) break;
                    
                    if (!tasks_.empty()) {
                        auto task = std::move(tasks_.front());
                        tasks_.pop();
                        lock.unlock();
                        task(); // выполнить задачу
                    }
                }
            });
        }
    }
    
    // Добавить задачу в пул
    template<typename F>
    void enqueue(F&& func) {
        {
            std::unique_lock<std::mutex> lock(task_mutex_);
            tasks_.push(std::forward<F>(func));
        }
        cv_.notify_one(); // пробудить один поток
    }
    
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(task_mutex_);
            shutdown_ = true;
        }
        cv_.notify_all(); // пробудить все потоки
        
        for (auto& worker : workers_) {
            worker.join(); // дождаться завершения
        }
    }
};

int main() {
    ThreadPool pool(4); // 4 потока (по количеству ядер CPU)
    
    // Добавляем 100k задач
    for (int i = 0; i < 100000; i++) {
        pool.enqueue([i] {
            processTask(i); // выполнится одним из 4 потоков
        });
    }
    // только 4 потока, не 100k! ✓
}

Преимущества Thread Pool

1. Переиспользование потоков

// Вместо создания потока для каждой задачи
// Существующий поток берёт задачу из очереди

Thread 1: task1 → task2 → task3 → task4 ...
Thread 2: task5 → task6 → task7 → task8 ...
Thread 3: task9 → task10 → ...
Thread 4: task11 → task12 → ...

// Все 100k задач обслуживают 4 потока!

2. Контроль над ресурсами

// 4 потока * 8MB стека = 32MB
// Вместо 100k потоков * 8MB = 800GB

ThreadPool pool(4); // чёткий контроль

// На большем сервере:
ThreadPool pool(16); // 16 потоков на 16-ядерном CPU

3. Оптимальный параллелизм

// Количество потоков = количество ядер CPU
int num_threads = std::thread::hardware_concurrency();
ThreadPool pool(num_threads);

// Нет чрезмерного context switching
// Каждое ядро выполняет свой поток

4. Предсказуемая производительность

// С thread pool
for (int i = 0; i < 1000000; i++) {
    pool.enqueue([i] { process(i); });
}
// Просто добавляем задачи в очередь
// Потоки обрабатывают их стабильно

// Без thread pool
for (int i = 0; i < 1000000; i++) {
    std::thread t([i] { process(i); }); // КРАХ! ❌
}

Сравнение производительности

// Тест: обработать 1 миллион задач

// Вариант 1: Прямое создание потоков
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; i++) {
    std::thread t([i] { simulate_work(i); });
    t.join();
}
auto elapsed1 = std::chrono::high_resolution_clock::now() - start;
// Может занять часы!

// Вариант 2: Thread Pool
start = std::chrono::high_resolution_clock::now();
ThreadPool pool(std::thread::hardware_concurrency());
for (int i = 0; i < 1000000; i++) {
    pool.enqueue([i] { simulate_work(i); });
}
auto elapsed2 = std::chrono::high_resolution_clock::now() - start;
// Может занять секунды!

// elapsed2 может быть в 100+ раз быстрее!

Современные решения

1. C++17: std::async с thread pool (неправильно)

// ❌ Это НЕ использует thread pool
// Каждый async может создать новый поток
for (int i = 0; i < 100000; i++) {
    auto future = std::async(std::launch::async, [i] {
        return process(i);
    });
}

2. Правильное использование std::async

// Иногда работает с thread pool (зависит от реализации)
auto future = std::async(std::launch::async, [] { });
// Но надёжнее использовать собственный pool

3. Современные библиотеки

// Boost.Asio: встроенный thread pool
// ZeroMQ: использует thread pool внутри
// gRPC: встроенный thread pool для обработки запросов
// Boost.Thread: можно написать собственный pool

Практический пример: HTTP сервер

❌ Неправильно: создание потока на запрос

class BadHttpServer {
public:
    void start(int port) {
        int server_socket = socket(AF_INET, SOCK_STREAM, 0);
        bind(server_socket, ...);
        listen(server_socket, SOMAXCONN);
        
        while (true) {
            int client_socket = accept(server_socket, ...);
            // ❌ Создаём новый поток для каждого соединения
            std::thread t([client_socket] {
                handleClient(client_socket);
            });
            t.detach(); // забыли про поток
        }
    }
};

✅ Правильно: thread pool

class GoodHttpServer {
private:
    ThreadPool pool_;
public:
    GoodHttpServer(int num_threads) : pool_(num_threads) {}
    
    void start(int port) {
        int server_socket = socket(AF_INET, SOCK_STREAM, 0);
        bind(server_socket, ...);
        listen(server_socket, SOMAXCONN);
        
        while (true) {
            int client_socket = accept(server_socket, ...);
            // ✓ Добавляем задачу в пул
            pool_.enqueue([client_socket] {
                handleClient(client_socket);
            });
        }
    }
};

Выбор размера pool

// Для CPU-bound задач
size_t pool_size = std::thread::hardware_concurrency();
// Обычно равно количеству ядер

// Для I/O-bound задач
size_t pool_size = std::thread::hardware_concurrency() * 2;
// Можно больше потоков, так как они часто в ожидании I/O

// Для сервера с множеством соединений
size_t pool_size = std::min(
    std::thread::hardware_concurrency() * 4,
    1024  // защита от избыточного использования памяти
);

Вывод

Прямое создание потоков:

  • ✓ Просто понять
  • ✗ Неэффективно для большого количества задач
  • ✗ Неконтролируемое использование ресурсов
  • ✗ Плохая масштабируемость

Thread Pool:

  • ✓ Переиспользование потоков
  • ✓ Контролируемое использование памяти и CPU
  • ✓ Хорошая масштабируемость
  • ✓ Предсказуемая производительность
  • ✗ Чуть сложнее в реализации

Правило: всегда используйте thread pool для высоконагруженных систем, обслуживающих множество задач или соединений. Это стандартная практика в production backend-сервисах.