Пулы потоков и процессов
Создавать поток на каждую задачу — расточительно. Пул переиспользует фиксированный набор воркеров.
Пул (pool) — заранее созданный фиксированный набор потоков или процессов-воркеров, которым раздаются задачи из общей очереди.
Если на каждую из тысячи задач создавать новый поток, вы утонете в накладных расходах: создание, переключение, память. Пул решает это: создаём, скажем, 8 воркеров один раз, складываем задачи в очередь, и воркеры разбирают их по мере освобождения.
concurrent.futures
Стандартная библиотека даёт единый удобный интерфейс — ThreadPoolExecutor (для I/O) и ProcessPoolExecutor (для CPU). Менять одно на другое — буквально одна строка.
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
return f"загружено {url}"
urls = [f"site{i}" for i in range(100)]
with ThreadPoolExecutor(max_workers=8) as pool:
# 8 воркеров разбирают 100 задач
results = list(pool.map(fetch, urls))
print(results[0], "...", len(results), "шт.")Для CPU-задач достаточно заменить класс на ProcessPoolExecutor — интерфейс тот же, но теперь воркеры это процессы, обходящие GIL.
Очередь задач на фиксированных воркерах
import collections
tasks = collections.deque(range(1, 7)) # 6 задач
workers = 2 # 2 воркера
round_no = 0
while tasks:
round_no += 1
batch = []
for _ in range(workers):
if tasks:
batch.append(tasks.popleft())
print(f"раунд {round_no}: воркеры взяли {batch}")Вывод:
раунд 1: воркеры взяли [1, 2] раунд 2: воркеры взяли [3, 4] раунд 3: воркеры взяли [5, 6]
Зачем ограничивать число воркеров
- Контроль ресурсов. 8 воркеров не положат сервер 10000 одновременными запросами.
- Меньше переключений. Не плодим тысячи потоков — меньше накладных расходов.
- Предсказуемость. Нагрузка ограничена сверху, систему легче рассчитать.
Как работает под капотом
Пул держит внутреннюю потокобезопасную очередь задач. Свободный воркер берёт задачу, выполняет её и возвращается за следующей. Результат каждой задачи оборачивается в объект Future — «обещание результата», который можно подождать (.result()) или опросить. map раздаёт задачи и собирает результаты по порядку, а submit возвращает отдельные Future, удобные, когда задачи завершаются вразнобой.
Частые ошибки
- Брать
ThreadPoolExecutorдля CPU-задач. GIL не даст ускорения — нуженProcessPoolExecutor. - Ставить
max_workersв огромное число. Тогда теряется весь смысл пула — снова тысячи потоков. - Не обрабатывать исключения в задачах. Ошибка внутри воркера всплывёт при
.result()— её надо ловить.
Итог
- Пул переиспользует фиксированный набор воркеров вместо создания нового на каждую задачу.
ThreadPoolExecutor— для I/O,ProcessPoolExecutor— для CPU.- Ограничение воркеров контролирует нагрузку и переключения.
- Результаты приходят через
Future; исключения всплывают при получении результата.