Какие механизмы конкурентности доступны в Python?

Ответ

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

  1. Потоки (Threading): Используются для I/O-bound задач, где основное время тратится на ожидание ввода/вывода (например, сетевые запросы, чтение/запись файлов). Потоки в Python не обеспечивают истинного параллелизма из-за Global Interpreter Lock (GIL), который позволяет выполнять только один поток байт-кода Python за раз. Однако, когда поток ожидает I/O, GIL освобождается, позволяя другим потокам выполняться.

    import threading
    import time
    
    def io_task(name):
        print(f"Поток {name}: Начинаю I/O операцию...")
        time.sleep(1) # Имитация I/O ожидания
        print(f"Поток {name}: I/O операция завершена.")
    
    thread1 = threading.Thread(target=io_task, args=("1",))
    thread2 = threading.Thread(target=io_task, args=("2",))
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()
    print("Все потоки завершены.")
  2. Процессы (Multiprocessing): Используются для CPU-bound задач, требующих интенсивных вычислений. Каждый процесс имеет свой собственный интерпретатор Python и, следовательно, свой собственный GIL. Это позволяет процессам выполняться параллельно на разных ядрах CPU, обходя ограничение GIL.

    from multiprocessing import Process
    import os
    
    def cpu_task(name):
        print(f"Процесс {name} (PID: {os.getpid()}): Начинаю CPU-интенсивную операцию...")
        _ = sum(i*i for i in range(10**7)) # Имитация вычислений
        print(f"Процесс {name}: CPU-интенсивная операция завершена.")
    
    process1 = Process(target=cpu_task, args=("A",))
    process2 = Process(target=cpu_task, args=("B",))
    
    process1.start()
    process2.start()
    
    process1.join()
    process2.join()
    print("Все процессы завершены.")
  3. Асинхронность (Asyncio): Оптимально для высококонкурентных I/O-bound задач, где требуется управлять большим количеством одновременных операций ввода/вывода (например, веб-серверы, работа с базами данных). asyncio использует один поток и один цикл событий (event loop) для кооперативной многозадачности. Корутины (функции, определённые с async def) добровольно передают управление циклу событий, когда сталкиваются с ожиданием I/O (await), позволяя другим корутинам выполняться.

    import asyncio
    import time
    
    async def async_io_task(name):
        print(f"Корутина {name}: Начинаю асинхронную I/O операцию...")
        await asyncio.sleep(1) # Имитация асинхронного I/O ожидания
        print(f"Корутина {name}: Асинхронная I/O операция завершена.")
    
    async def main():
        await asyncio.gather(
            async_io_task("X"),
            async_io_task("Y")
        )
    
    asyncio.run(main())
    print("Все асинхронные задачи завершены.")
  4. concurrent.futures: Это высокоуровневый модуль, предоставляющий абстракцию для запуска задач в пулах потоков (ThreadPoolExecutor) или процессов (ProcessPoolExecutor). Он упрощает управление конкурентными задачами, позволяя отправлять функции для выполнения и получать их результаты через объекты Future.

    from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
    import time
    
    def simple_task(name):
        time.sleep(0.5)
        return f"Задача {name} выполнена."
    
    # Пример с ThreadPoolExecutor (для I/O-bound)
    with ThreadPoolExecutor(max_workers=2) as executor:
        future1 = executor.submit(simple_task, "A")
        future2 = executor.submit(simple_task, "B")
        print(future1.result())
        print(future2.result())
    
    # Пример с ProcessPoolExecutor (для CPU-bound)
    # with ProcessPoolExecutor(max_workers=2) as executor:
    #     future1 = executor.submit(simple_task, "C")
    #     future2 = executor.submit(simple_task, "D")
    #     print(future1.result())
    #     print(future2.result())

Выбор подходящего механизма зависит от характера задачи: threading для I/O-bound с небольшим количеством потоков, multiprocessing для CPU-bound, asyncio для высококонкурентных I/O-bound задач, и concurrent.futures как удобная обёртка для обоих.

Ответ 18+ 🔞

А, ну это же про то, как в Питоне заставить всё работать одновременно, да? Ну, слушай, тут целый зоопарк, блядь, и каждый зверь для своего дела. Ёпта, сейчас разберёмся, кто на что горазд.

Вот смотри, есть у нас несколько способов, и если выбрать не тот — будет как Герасим с Муму, просто утопишь производительность, блядь, в пруду.

1. Потоки (Threading) Это, типа, для задач, где ты в основном ждёшь. Сеть там, файлы почитай-запиши. Но тут есть подвох, ёпта — GIL, эта самая глобальная блокировка интерпретатора. Она как злобная бабка-консьержка: в один момент времени только один поток может реально питоньи инструкции выполнять. Но! Пока поток спит, ожидая ответа от сети, бабка (GIL) отпускает его и пускает другого. Так что для ожидания — самое то.

import threading
import time

def io_task(name):
    print(f"Поток {name}: Начинаю I/O операцию...")
    time.sleep(1) # Имитация I/O ожидания
    print(f"Поток {name}: I/O операция завершена.")

thread1 = threading.Thread(target=io_task, args=("1",))
thread2 = threading.Thread(target=io_task, args=("2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
print("Все потоки завершены.")

2. Процессы (Multiprocessing) А вот это уже тяжёлая артиллерия, для задач, где мозги процессора наизнанку выворачиваешь — вычисления всякие, блядь. Тут каждый процесс — это как отдельная, блядь, квартира со своим собственным интерпретатором и своей бабкой-консьержкой (GIL). И они уже реально параллельно на разных ядрах процессора могут пахать. Запустил — и поехали.

from multiprocessing import Process
import os

def cpu_task(name):
    print(f"Процесс {name} (PID: {os.getpid()}): Начинаю CPU-интенсивную операцию...")
    _ = sum(i*i for i in range(10**7)) # Имитация вычислений
    print(f"Процесс {name}: CPU-интенсивная операция завершена.")

process1 = Process(target=cpu_task, args=("A",))
process2 = Process(target=cpu_task, args=("B",))

process1.start()
process2.start()

process1.join()
process2.join()
print("Все процессы завершены.")

3. Асинхронность (Asyncio) О, это хитрая жопа, блядь. Для случаев, когда у тебя тысяча мелких задач, которые всё время чего-то ждут. Веб-сервер, например. Работает в одном потоке, по принципу "я сейчас подожду, а ты пока поработай". Функции (async def) говорят await, и управление добровольно отдают главной петле событий. Кооперация, ёпта, а не конкуренция. Но если какая-то корутина забудет про await и начнёт считать — всё, стоп-кран, все остальные будут ждать, как дурачки.

import asyncio
import time

async def async_io_task(name):
    print(f"Корутина {name}: Начинаю асинхронную I/O операцию...")
    await asyncio.sleep(1) # Имитация асинхронного I/O ожидания
    print(f"Корутина {name}: Асинхронная I/O операция завершена.")

async def main():
    await asyncio.gather(
        async_io_task("X"),
        async_io_task("Y")
    )

asyncio.run(main())
print("Все асинхронные задачи завершены.")

4. concurrent.futures Ну это такой красивый фасад, чтобы не париться с потоками и процессами вручную. Сказал: "Хочу пул из двух потоков" — и отправляй туда задачи. Он тебе вернёт некий "билет" (Future), по которому потом результат получишь. Удобно, бля, как тургеневская барыня, всё за тебя сделает.

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time

def simple_task(name):
    time.sleep(0.5)
    return f"Задача {name} выполнена."

# Пример с ThreadPoolExecutor (для I/O-bound)
with ThreadPoolExecutor(max_workers=2) as executor:
    future1 = executor.submit(simple_task, "A")
    future2 = executor.submit(simple_task, "B")
    print(future1.result())
    print(future2.result())

# Пример с ProcessPoolExecutor (для CPU-bound)
# with ProcessPoolExecutor(max_workers=2) as executor:
#     future1 = executor.submit(simple_task, "C")
#     future2 = executor.submit(simple_task, "D")
#     print(future1.result())
#     print(future2.result())

Короче, суть в чём: если ждёшь — потоки или асинки, если считаешь — процессы. А concurrent.futures — это такая универсальная отмазка, когда не хочешь глубоко вникать. Выбирай с умом, а то будет как в том анекдоте: "Муму-муму", блядь, а она уже на дне.