Распараллеливание ускоряет программу, когда несколько частей работы можно выполнять независимо друг от друга.
Например, скрипт должен проверить 100 URL-адресов. Проверка одной страницы не влияет на проверку другой, поэтому запросы можно запускать одновременно.
Главный вопрос: может ли задача начать выполняться, не дожидаясь результата другой задачи?
Главный критерий — независимость
Задача подходит для параллельного выполнения, если её можно разделить на отдельные части без нарушения результата.
Можно распараллелить
- скачивание файлов по списку ссылок;
- запросы к API для множества товаров или клиентов;
- проверку доступности сайтов;
- отправку уведомлений нескольким получателям;
- обработку независимых файлов в папке;
- парсинг нескольких независимых страниц.
URL 2 → статус
URL 3 → статус
URL 4 → статус
Нельзя распараллелить
Если следующий шаг зависит от предыдущего, порядок важен.
Нельзя обработать файл до его скачивания. Но если файлов много, можно параллельно скачивать их или обрабатывать каждый файл после загрузки.
Что именно тормозит программу
Перед добавлением потоков нужно понять тип нагрузки.
| Тип задачи | Что делает программа | Подходящий инструмент |
|---|---|---|
| I/O-bound | Ждёт сеть, API, файлы или базу данных | ThreadPoolExecutor, asyncio |
| CPU-bound | Активно использует процессор | ProcessPoolExecutor |
| Зависимая задача | Каждый этап требует результат предыдущего | Последовательный код |
Потоки особенно полезны для I/O-задач. Пока один поток ждёт ответ API, другой может отправить следующий запрос.
Для тяжёлых вычислений потоки часто не ускоряют Python-код из-за GIL. В таком случае лучше использовать процессы или библиотеки, которые выполняют вычисления вне Python-интерпретатора: NumPy, Polars, PyTorch и другие.
Пример: последовательная проверка URL
Функция ниже имитирует запрос длительностью одну секунду. Пять URL последовательно обрабатываются примерно за пять секунд.
Скачать Jupyter Notebook Открыть в Google Colab
import time
from concurrent.futures import ThreadPoolExecutor
urls = [
'https://example.com/page-1',
'https://example.com/page-2',
'https://example.com/page-3',
'https://example.com/page-4',
'https://example.com/page-5'
]
def check_url(url):
time.sleep(1)
return f'{url}: OK'
start_time = time.perf_counter()
results = [check_url(url) for url in urls]
elapsed = time.perf_counter() - start_time
print(*results, sep='\n')
print(f'\nВремя: {elapsed:.2f} сек.')
https://example.com/page-1: OK https://example.com/page-2: OK https://example.com/page-3: OK https://example.com/page-4: OK https://example.com/page-5: OK Время: 5.00 сек.
Та же задача в несколько потоков
URL независимы, поэтому их можно проверить через пул потоков.
start_time = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(check_url, urls))
elapsed = time.perf_counter() - start_time
print(*results, sep='\n')
print(f'\nВремя: {elapsed:.2f} сек.')
https://example.com/page-1: OK https://example.com/page-2: OK https://example.com/page-3: OK https://example.com/page-4: OK https://example.com/page-5: OK Время: 1.01 сек.
Теперь все задачи начинают работу почти одновременно. Пять ожиданий по одной секунде занимают примерно одну секунду, а не пять.
Когда потоки могут навредить
Параллельность добавляет сложность. Не стоит использовать её без необходимости.
- Есть общий ресурс. Несколько потоков не должны без синхронизации одновременно записывать в один файл, менять общий счётчик или DataFrame.
-
Есть лимиты API. Сервис может вернуть ошибку
429 Too Many Requestsили временно заблокировать клиента. - Задача слишком маленькая. Если работа занимает доли секунды, создание потоков может не дать заметной выгоды.
Сколько потоков использовать
Не нужно создавать поток на каждую задачу. Если у вас 10 000 URL, запуск 10 000 одновременных запросов создаст лишнюю нагрузку и может привести к блокировке.
Для начала можно выбрать 5–10 потоков и измерить результат. Затем стоит поэкспериментировать с количеством потоков, чтобы найти оптимальное значение: слишком мало — задача выполняется дольше, слишком много — сервер начинает отклонять запросы (ошибки 429 или 503).
Чек-лист
- Есть ли много одинаковых операций для разных объектов?
- Независимы ли эти операции друг от друга?
- Программа ждёт сеть, API, диск или базу данных?
- Есть ли лимиты у внешнего API?
- Измерили ли вы ускорение после изменений?
Вывод
Распараллеливать стоит независимые задачи. В Python потоки хорошо подходят для запросов к API, загрузки файлов, парсинга сайтов и других операций, где программа в основном ждёт.
Если задача нагружает процессор, лучше рассмотреть процессы. Если шаги зависят друг от друга, последовательный код часто остаётся самым надёжным решением.
Сначала найдите, что можно делать независимо, и только после этого решайте, имеет ли смысл добавлять параллельность.