Простой способ работать с конкурентностью и параллелизмом в Python

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

Python предлагает очень простые средства для решения таких задач прямо в стандартной библиотеке — пул исполнителей (pool executors).


Что такое пул исполнителей

Пул исполнителей — это абстракция, которая скрывает детали создания и управления потоками или процессами. Вы просто говорите: «выполни эти задачи параллельно», а Python сам раздает их рабочим.

В Python это реализовано через модуль concurrent.futures, а сами исполнители — ThreadPoolExecutor (пул потоков) и ProcessPoolExecutor (пул процессов).


Пример: конкурентное скачивание страниц

Рассмотрим классическую задачу: у вас есть список URL-адресов, и нужно скачать каждую страницу и распечатать <title> из HTML. Если делать это последовательно, значит одна страница будет скачана за другой и весь процесс займет много времени из-за сетевых задержек:

import time
import re
from urllib.request import Request, urlopen

URLs = [
    "https://example.com/page1",
    "https://example.com/page2",
    # ... ещё ссылки
]

title_pattern = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE)

def fetch_url(url):
    start = time.time()
    with urlopen(Request(url, headers={"User-Agent":"Mozilla/5.0"}) ) as response:
        html = response.read().decode("utf-8")
        match = title_pattern.search(html)
        title = match.group(1) if match else "Unknown"
    end = time.time()
    print(f"Fetched {url} in {end - start:.2f}s")
    return title

start = time.time()
for url in URLs:
    title = fetch_url(url)
    print(url, title)
print("Total:", time.time() - start)

Такой код будет ждать завершения каждого запроса перед переходом к следующему. Если страница долго отвечает, то скрипт простаивает.


Улучшение с ThreadPoolExecutor

Теперь тот же пример, но с пулом потоков:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=len(URLs)) as executor:
    futures = {executor.submit(fetch_url, url): url for url in URLs}

    for future in as_completed(futures):
        url = futures[future]
        title = future.result()
        print(f"{url} -> {title}")

Что здесь происходит:

  • executor.submit(fetch_url, url) отправляет задачу в пул и сразу возвращает объект Future.

  • as_completed позволяет обрабатывать результаты по мере готовности, а не в порядке отправки.

Преимущество: пока один поток ждет сетевого ответа, другой уже выполняет свою работу. За счет этого общее время выполнения сокращается почти до максимального ответа из всех запросов.


Потоки vs процессы

Когда использовать потоки (ThreadPoolExecutor)

Пулы потоков подходят для операций ввода-вывода:

  • сетевые запросы;

  • чтение/запись файлов;

  • любые задержки, связанные с внешними сервисами.

Потоки здесь эффективны, потому что вся задержка не в вашем Python-коде, а во внешних системах.


Когда использовать процессы (ProcessPoolExecutor)

Процессы нужны, когда задачи CPU-bound, то есть нагружают процессор:

  • тяжёлые вычисления;

  • обработка больших массивов данных;

  • работа, не освобождающая GIL (глобальную блокировку интерпретатора Python).

Процессы позволяют использовать несколько ядер процессора, параллельно выполняя задачи на уровне OS-процессов, минуя ограничения GIL.

Пример замены:

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=5) as executor:
    results = executor.map(cpu_intensive_task, inputs)

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


Когда это действительно полезно

Пулы исполнителей не решают все возможные проблемы многозадачности, но они идеально подходят для:

  • ускорения IO-bound операций;

  • обработки многозадачных, но коротких задач;

  • избегания блокировки основного потока (например, UI-потока);

  • написания аккуратного и понятного кода, который намного проще, чем вручную управлять потоками и процессами.


Примеры реальных задач

Скрапинг сайта

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

with ThreadPoolExecutor(max_workers=20) as executor:
    futures = [executor.submit(scrape, url) for url in list_of_urls]

Архивация файлов

Если надо упаковать десятки больших файлов, пул процессов позволит разделить работу по ядрам:

with ProcessPoolExecutor(max_workers=4) as executor:
    executor.map(zip_file, files)

Важные нюансы

  • Помните про GIL: в Python потоки не дают настоящего параллелизма CPU-bound задачам. Тут помогут именно процессы.

  • Объекты Future позволяют получать результат задачи или ловить исключения.

  • Вызов future.result() блокирует до готовности результата, поэтому стоит использовать as_completed, map или другие обходы.

  • Для multiprocessing в Windows обязательно используйте защиту if __name__ == "__main__" чтобы избежать бесконечного порождения процессов.

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

Войдите, чтобы оставить комментарий

Похожие статьи

Почему простые архитектуры выигрывают: уроки системного дизайна от инженера GitHub

Почему сложность в архитектуре - зло, и как инженеры GitHub подходят к системному дизайну без фанатизма. Простые решения, вертикальное масштабирование, эволюция вместо переписывания и разбор ключевых идей.

40 0 1 мин

jQuery 4.0: крупный релиз спустя почти десять лет

jQuery 4.0 вышла в январе 2026 года. Это первый крупный релиз за почти десять лет. Он обновляет библиотеку под современные стандарты, снимает поддержку старых браузеров, добавляет защиту через Trusted Types и делает код легче и безопаснее.

39 0 1 мин

Работа с изображениями в Laravel через пакет Intervention Image

Как использовать библиотеку Intervention Image в Laravel для обработки изображений. Установка, чтение, изменение размера, обрезка, конвертация и водяные знаки. Практические примеры кода и советы по применению.

60 0 2 мин