Параллельные миры 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.
Данные между процессами не разделяются (нужно использовать
Queue
,Pipe
или 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.