IT Blog

Интересные факты из аналитики, разработки, автоматизации и AI

← Все статьи

Как понять, что задачу можно распараллелить

Не каждую медленную программу нужно запускать в нескольких потоках. Разбираем главный критерий распараллеливания, разницу между I/O- и CPU-задачами и пример с ThreadPoolExecutor.

Распараллеливание ускоряет программу, когда несколько частей работы можно выполнять независимо друг от друга.

Например, скрипт должен проверить 100 URL-адресов. Проверка одной страницы не влияет на проверку другой, поэтому запросы можно запускать одновременно.

Главный вопрос: может ли задача начать выполняться, не дожидаясь результата другой задачи?

Главный критерий — независимость

Задача подходит для параллельного выполнения, если её можно разделить на отдельные части без нарушения результата.

Можно распараллелить

  • скачивание файлов по списку ссылок;
  • запросы к API для множества товаров или клиентов;
  • проверку доступности сайтов;
  • отправку уведомлений нескольким получателям;
  • обработку независимых файлов в папке;
  • парсинг нескольких независимых страниц.
Пример независимых операций: проверка доступности сайтов
URL 1 → статус
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 последовательно обрабатываются примерно за пять секунд.

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).

Чек-лист

  1. Есть ли много одинаковых операций для разных объектов?
  2. Независимы ли эти операции друг от друга?
  3. Программа ждёт сеть, API, диск или базу данных?
  4. Есть ли лимиты у внешнего API?
  5. Измерили ли вы ускорение после изменений?

Вывод

Распараллеливать стоит независимые задачи. В Python потоки хорошо подходят для запросов к API, загрузки файлов, парсинга сайтов и других операций, где программа в основном ждёт.

Если задача нагружает процессор, лучше рассмотреть процессы. Если шаги зависят друг от друга, последовательный код часто остаётся самым надёжным решением.

Сначала найдите, что можно делать независимо, и только после этого решайте, имеет ли смысл добавлять параллельность.
← Все статьи