Когда стандартные Pool из concurrent.futures не дают гибкости для приоритетов и динамического распределения ресурсов, приходится писать свой пул. И тут ключевой выбор — preemptive или cooperative вытеснение. В production это критично, когда CPU-bound задачи конкурируют за ядра и время.
Preemptive: управление на уровне ОС
Используешь multiprocessing с отдельными процессами. ОС сама решает, когда переключать контекст, и ни один воркер не зависнет надолго. Минус — оверхед на межпроцессное взаимодействие и сериализацию. Плюс — честное распределение CPU. Для high-priority задач выставляешь nice процессу — и гарантируешь приоритет.
Cooperative: иллюзия параллелизма под CPU
asyncio и gevent — всё в одном процессе, оверхед минимален. Но под CPU-bound это ловушка: если воркер не отдаст управление через await, пул встанет. Костыль вроде asyncio.to_thread перегружает GIL и теряет преимущества. Только для гибридных сценариев: I/O на asyncio, CPU-блоки в ThreadPoolExecutor с флагом границы переключения.
Реальные кейсы
* Тяжелые вычисления с приоритетами — только preemptive. High-priority задача выполнится, игнорируя low-priority.
* Микро-батчи с изменяемым размером пула — multiprocessing с динамическим добавлением воркеров удобен. Но без контроля жизненного цикла процессы утекают.
# Упрощенный фрагмент ручного управления
worker = Process(target=run, args=(queue,))
worker.start()
# При смене нагрузки:
worker.terminate()
Типичная ошибка
Пытаться сделать cooperative-пул под CPU-bound без изоляции. Чистое asyncio под нагрузкой — это deadlock. А кооперативность через треды с GIL — не даёт масштабирования. Preemptive правильнее для любой CPU-bound задачи, даже если это кажется тяжеловесным.
Вывод:
Для CPU-bound production выбирай preemptive через multiprocessing с ручным жизненным циклом, а cooperative оставь для I/O, где GIL не ограничивает.