Как достичь параллелизма в Python с учетом GIL?

«Как достичь параллелизма в Python с учетом GIL?» — вопрос из категории Асинхронность, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

Выбор правильного подхода зависит от типа задачи.

1. Для CPU-bound задач: multiprocessing

Этот модуль обходит GIL, создавая отдельные процессы операционной системы. Каждый процесс имеет свой собственный интерпретатор Python и, соответственно, свой собственный GIL. Это единственный способ добиться истинного параллелизма для вычислений на нескольких ядрах CPU.

Пример:

from multiprocessing import Pool

def square(x):
    # Это CPU-bound задача
    return x * x

if __name__ == '__main__':
    # Запускаем 4 рабочих процесса
    with Pool(4) as p:
        results = p.map(square, [1, 2, 3, 4, 5, 6, 7, 8])
        print(results)  # Вывод: [1, 4, 9, 16, 25, 36, 49, 64]

2. Для I/O-bound задач: threading или asyncio

В задачах, ограниченных вводом-выводом (I/O), программа большую часть времени ждет ответа от сети, диска или базы данных. В эти моменты GIL освобождается, позволяя другим потокам выполняться.

  • threading: Использует вытесняющую многозадачность на уровне ОС. Подходит для интеграции с блокирующим кодом.

    import threading
    import requests
    
    def fetch_url(url):
        # GIL освобождается во время ожидания ответа от сети
        response = requests.get(url)
        print(f"Fetched {url}, status: {response.status_code}")
    
    urls = ["https://google.com", "https://bing.com"]
    threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
    for t in threads: t.start()
    for t in threads: t.join()
  • asyncio: Использует кооперативную многозадачность в одном потоке. Более эффективно по памяти и производительности для тысяч одновременных I/O операций.

    import asyncio
    import aiohttp
    
    async def fetch_url(session, url):
        async with session.get(url) as response:
            print(f"Fetched {url}, status: {response.status}")
    
    async def main():
        urls = ["https://google.com", "https://bing.com"]
        async with aiohttp.ClientSession() as session:
            tasks = [fetch_url(session, url) for url in urls]
            await asyncio.gather(*tasks)
    
    asyncio.run(main())

Итог:

  • CPU-bound: Используйте multiprocessing.
  • I/O-bound: Используйте threading для простоты или asyncio для максимальной производительности.