Какие подходы к управлению параллелизмом и конкурентностью существуют в Python

Ответ

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

  1. Многопоточность (threading) Используется для конкурентного выполнения задач, ограниченных вводом-выводом (I/O-bound), таких как сетевые запросы или чтение с диска. Из-за Global Interpreter Lock (GIL) потоки в CPython не могут выполняться параллельно на нескольких ядрах CPU, но могут эффективно переключаться между собой во время ожидания I/O.

    import threading
    import time
    
    def network_request(url):
        print(f"Fetching {url}...")
        time.sleep(1) # Имитация ожидания ответа от сети
        print(f"Finished {url}")
    
    threads = [
        threading.Thread(target=network_request, args=(f"site.com/{i}",))
        for i in range(3)
    ]
    
    for t in threads: t.start()
    for t in threads: t.join()
  2. Многопроцессорность (multiprocessing) Обеспечивает истинный параллелизм, запуская задачи в отдельных процессах, каждый со своим интерпретатором Python и памятью. Это позволяет обойти GIL и полностью утилизировать все ядра CPU.

    Почему это полезно? Идеально для задач, ограниченных производительностью процессора (CPU-bound), например, математические вычисления, обработка изображений.

    from multiprocessing import Process
    
    def cpu_heavy_task(n):
        result = sum(i * i for i in range(n))
        print(f"Task finished with result: {result}")
    
    processes = [
        Process(target=cpu_heavy_task, args=(10**7,))
        for _ in range(3)
    ]
    
    for p in processes: p.start()
    for p in processes: p.join()
  3. Асинхронность (asyncio) Современный подход для конкурентного выполнения большого количества I/O-bound задач в одном потоке с помощью событийного цикла (event loop). Код пишется с использованием синтаксиса async/await.

    Почему это полезно? Очень эффективно для приложений с десятками тысяч одновременных сетевых соединений (веб-серверы, боты, парсеры).

    import asyncio
    
    async def network_request_async(url):
        print(f"Fetching {url}...")
        await asyncio.sleep(1) # Неблокирующее ожидание
        print(f"Finished {url}")
    
    async def main():
        tasks = [network_request_async(f"site.com/{i}") for i in range(3)]
        await asyncio.gather(*tasks)
    
    asyncio.run(main())

Ключевой фактор выбора:

  • I/O-bound: threading или asyncio.
  • CPU-bound: multiprocessing.

Ответ 18+ 🔞

Давай разберём эту тему, а то я вижу, у людей тут иногда в голове каша, будто они муку с цементом перепутали.

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

1. Многопоточность (threading) Это когда ты хочешь, чтобы твоя программа не тупила, пока ждёт ответа от какого-нибудь медленного сервера или читает с диска. Задачи тут конкурентные, то есть они по очереди быстро-быстро переключаются, пока одна ждёт. Но есть одна огромная, жирная, блядь, проблема — GIL (Global Interpreter Lock). Эта штука в CPython не даёт потокам реально работать параллельно на нескольких ядрах процессора. Они как мартышки на одной верёвке: дергаются все, но бегут по одной.

import threading
import time

def network_request(url):
    print(f"Fetching {url}...")
    time.sleep(1) # Прикидываемся, что ждём ответ из сети
    print(f"Finished {url}")

threads = [
    threading.Thread(target=network_request, args=(f"site.com/{i}",))
    for i in range(3)
]

for t in threads: t.start()
for t in threads: t.join()

Короче, если твоя программа много ждёт (сеть, диск) — это твой вариант. Хуй с ним, с GIL, пока одна нить спит, другая может работать.

2. Многопроцессорность (multiprocessing) А вот это уже серьёзно, ёпта! Это когда тебе нужно настоящее параллельное выполнение, чтобы загрузить все ядра твоего процессора по полной программе. Здесь каждая задача запускается в отдельном процессе, со своим собственным интерпретатором и памятью. GIL? Да похуй на него! Он в каждом процессе свой, так что они не мешают друг другу.

Зачем это надо? Представь, что тебе нужно перемолоть кучу чисел, обработать тонну изображений или что-то такое, где процессор должен пахать, а не ждать. Вот для таких CPU-bound задач — это твой выбор, золотой.

from multiprocessing import Process

def cpu_heavy_task(n):
    result = sum(i * i for i in range(n))
    print(f"Task finished with result: {result}")

processes = [
    Process(target=cpu_heavy_task, args=(10**7,))
    for _ in range(3)
]

for p in processes: p.start()
for p in processes: p.join()

Запустишь — и твой комп загудит как пылесос, потому что все ядра в деле. Красота!

3. Асинхронность (asyncio) А это, блядь, модная молодёжная тема. Современный подход, чтобы в одном потоке управлять кучей I/O-задач. Всё крутится вокруг событийного цикла (event loop) и магии async/await.

Когда это охуенно? Когда у тебя не три запроса к сайту, а тридцать тысяч. Веб-серверы, чат-боты, парсеры — всё, что работает с сетью и должно обслуживать дохуище соединений одновременно, не создавая дохуища потоков или процессов.

import asyncio

async def network_request_async(url):
    print(f"Fetching {url}...")
    await asyncio.sleep(1) # Тут мы не блокируем весь мир, а просто говорим "разбудите через секунду"
    print(f"Finished {url}")

async def main():
    tasks = [network_request_async(f"site.com/{i}") for i in range(3)]
    await asyncio.gather(*tasks)

asyncio.run(main())

Выглядит непривычно, но зато эффективно, как хитрая жопа.

Итог, чтобы не ебать себе мозг:

  • Задача много ждёт (I/O-bound): Бери threading или учи asyncio.
  • Задача много считает (CPU-bound): Только multiprocessing, иначе GIL превратит твои потоки в одного унылого работягу.

Выбирай с умом, а то потом будешь сидеть и думать: «Ну почему же оно такое медленное, ебать мои старые костыли!»