Как стать автором
Обновить

Параллельные миры Python: threading, multiprocessing и asyncio в бою

Уровень сложностиСредний

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

Вы пишете код на Python и столкнулись с медленной работой приложения? Возможно, вы используете не тот подход для параллельных задач. Один разработчик заменил threading на asyncio и ускорил API в 5 раз, другой — применил multiprocessing и сократил время обработки данных с часов до минут. Но как понять, что подойдёт именно вам? Давайте разберёмся, как работают эти инструменты, и главное — когда их стоит (или не стоит) применять.


GIL: «Сторож» Python, который всех бесит

Представьте, что интерпретатор Python — это кухня с одним поваром (потоком), который готовит блюда (выполняет код). GIL (Global Interpreter Lock) — это строгий шеф-повар, который не даёт другим поварам зайти на кухню, пока один работает. Кажется, это глупо? Но именно так устроен CPython!

Как GIL влияет на ваши задачи:

  • CPU-bound задачи (математика, ML, рендеринг): GIL превращает многопоточность в иллюзию — код выполняется только в одном потоке.

  • I/O-bound задачи (сеть, файлы, API): GIL отпускает поток во время ожидания, давая дорогу другим.

💡 Миф: «GIL есть только в Python». Нет! Например, в Ruby (MRI) тоже есть аналогичный механизм.


threading: Лёгкие потоки для I/O операций

Как работает:

  • Все потоки живут в одном процессе.

  • Переключение между потоками происходит при блокирующих вызовах (например, time.sleep() или запросе к API).

import threading

def fetch_data(url):
    print(f"Загрузка {url}...")
    # Имитация долгого запроса
    time.sleep(2)
    print(f"Данные с {url} получены!")

# Запуск трёх потоков для параллельных HTTP-запросов
threads = [threading.Thread(target=fetch_data, args=(f"https://api.site.com/{i}",)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Плюсы ✅:

  • Минимальные накладные расходы на создание.

  • Идеально для одновременных HTTP-запросов, работы с файлами.

Минусы ❌:

  • Бесполезны для вычислений (спасибо GIL!).

  • Риск deadlock, если неаккуратно использовать Lock.


multiprocessing: Настоящий параллелизм для CPU-задач

Как работает:

  • Каждый процесс — отдельный экземпляр Python с своим GIL.

  • Данные между процессами не разделяются (нужно использовать QueuePipe или shared memory).

from multiprocessing import Process
import hashlib

def hash_password(password):
    # Ресурсоёмкая операция хеширования
    hashlib.scrypt(password.encode(), salt=b'salt', n=2**14).hex()
    print(f"Пароль обработан")

# Запуск процессов для параллельного хеширования
processes = [Process(target=hash_password, args=(f"qwerty{i}",)) for i in range(4)]
for p in processes:
    p.start()
for p in processes:
    p.join()

Плюсы ✅:

  • Обходит GIL — использует все ядра CPU.

  • Изоляция процессов: крах одного не убьёт всю программу.

Минусы ❌:

  • Высокое потребление памяти.

  • Сложность обмена данными между процессами.


asyncio: Асинхронность для 10k+ соединений

Как работает:

  • Использует корутины и событийный цикл.

  • Нет переключения потоков — вместо этого задачи «договариваются» об ожидании.

import asyncio

async def handle_client(reader, writer):
    data = await reader.read(100)
    await asyncio.sleep(1)  # Имитация обработки
    writer.write(b"OK")
    await writer.drain()

async def main():
    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)
    async with server:
        await server.serve_forever()

asyncio.run(main())

Плюсы ✅:

  • Обрабатывает тысячи одновременных соединений (например, чат-серверы).

  • Минимальные задержки на переключение контекста.

Минусы ❌:

  • Всё должно быть асинхронным: от HTTP-клиента до PostgreSQL-драйвера.

  • Сложная отладка из-за неочевидного порядка выполнения.


Что выбрать? Решаем на реальных кейсах

Сценарий

threading

multiprocessing

asyncio

Парсинг 100 сайтов

✅ (но Pool лучше)

❌ (избыточно)

✅ (лучший выбор)

Обработка видео

✅ (полный CPU)

Веб-сокет сервер

❌ (масштаб)

Массовая обработка PDF

⚠️ (если есть I/O)

✅ (если CPU-heavy)

⚠️ (сложно)


Продвинутые сценарии

Комбинируем подходы: asyncio + процессы

import asyncio
from concurrent.futures import ProcessPoolExecutor

async def run_in_process(func, *args):
    loop = asyncio.get_event_loop()
    with ProcessPoolExecutor() as pool:
        return await loop.run_in_executor(pool, func, *args)

async def main():
    result = await run_in_process(cpu_intensive_task, data)

ThreadPool для блокирующего кода в asyncio

# Если библиотека не поддерживает asyncio
await loop.run_in_executor(ThreadPoolExecutor(), blocking_io_function)

Заключение: не гонитесь за модой

  • asyncio — не «серебряная пуля». Если у вас 10 одновременных запросов, threading проще.

  • multiprocessing может быть избыточным для простых скриптов — используйте его для тяжёлых вычислений.

  • Важно: Перед оптимизацией замеряйте производительность! Иногда проблема в алгоритме, а не в GIL.

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.