Pull to refresh

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

Level of difficultyMedium

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

Вы пишете код на 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.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.