← Назад к вопросам

Какая особенность параллельности в Python?

2.3 Middle🔥 171 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Особенность параллельности в Python: GIL

Параллельность в Python имеет одну критическую особенность, которая отличает её от других языков программирования — Global Interpreter Lock (GIL). Это одна из самых важных концепций для понимания производительности многопоточного кода на Python.

Что такое GIL?

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

import threading
import time

def cpu_bound_work():
    """Вычислительная работа без I/O"""
    total = 0
    for i in range(100_000_000):
        total += i
    return total

# Последовательное выполнение: ~3 секунды
start = time.time()
result1 = cpu_bound_work()
result2 = cpu_bound_work()
print(f"Sequential: {time.time() - start:.2f}s")  # ~3.0s

# Многопоточное выполнение: ~5 секунд (медленнее!)
start = time.time()
threads = [
    threading.Thread(target=cpu_bound_work),
    threading.Thread(target=cpu_bound_work),
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Threaded: {time.time() - start:.2f}s")  # ~5.0s (из-за GIL!)

Почему медленнее? GIL создает контекст-switching overhead, который может быть дороже самих вычислений.

Почему существует GIL?

GIL был добавлен в CPython по практическим причинам:

  1. Простота реализации памяти — не нужны сложные механизмы синхронизации
  2. Совместимость с C расширениями — большинство C библиотек не потокобезопасны
  3. Производительность однопоточного кода — нет overhead синхронизации
// Упрощенно: как работает GIL в CPython
struct PyInterpreterState {
    PyObject *modules;  // Глобальные переменные
    PyMem_SetAllocator(); // Выделение памяти
};

// Перед выполнением Python кода:
PyEval_AcquireLock();  // Получить GIL
try {
    execute_python_code();
} finally {
    PyEval_ReleaseLock();  // Освободить GIL
}

Когда GIL НЕ блокирует

Есть операции, которые освобождают GIL и позволяют другим потокам работать:

import threading
import time
import requests  # I/O операция

def io_bound_work():
    """I/O операция — GIL освобождается"""
    response = requests.get("https://api.example.com/data")  # GIL отпущен!
    return response.json()

def sleep_work():
    """time.sleep() освобождает GIL"""
    time.sleep(2)  # GIL отпущен, другие потоки могут работать
    return "Done"

# Параллельное выполнение I/O: ~2 секунды (эффективно!)
start = time.time()
threads = [
    threading.Thread(target=io_bound_work),
    threading.Thread(target=io_bound_work),
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"I/O Threaded: {time.time() - start:.2f}s")  # ~2.0s ✅

Освобождают GIL:

  • I/O операции (читать файлы, сетевые запросы)
  • time.sleep()
  • Вызовы C расширений (NumPy, Pillow)
  • Некоторые встроенные функции

Решения для CPU-bound задач

1. Multiprocessing (отдельные процессы)

from multiprocessing import Pool
import time

def cpu_bound_work(n):
    total = 0
    for i in range(n):
        total += i
    return total

# CPU-bound на 4 ядрах: ~0.75 секунды (4 процесса, параллельно)
if __name__ == "__main__":
    start = time.time()
    with Pool(4) as pool:
        results = pool.map(cpu_bound_work, [100_000_000] * 4)
    print(f"Multiprocessing: {time.time() - start:.2f}s")  # ~0.75s ✅

Плюсы:

  • Истинная параллельность на многоядерных процессорах
  • Не зависит от GIL

Минусы:

  • Процессы требуют больше памяти
  • Медленнее процесс создания и общение между процессами
  • Сложнее делиться состоянием

2. Asyncio (асинхронность)

import asyncio
import aiohttp

async def fetch_url(session, url):
    """Асинхронный I/O — GIL освобождается автоматически"""
    async with session.get(url) as response:
        return await response.text()

async def main():
    """Параллельные I/O запросы"""
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_url(session, "https://api.example.com/1"),
            fetch_url(session, "https://api.example.com/2"),
            fetch_url(session, "https://api.example.com/3"),
        ]
        results = await asyncio.gather(*tasks)  # Все выполняются параллельно
    return results

# Запуск: все 3 запроса параллельно (~1 сек вместо 3)
asyncio.run(main())

Плюсы:

  • Легкие корутины, низкий overhead
  • Идеален для I/O-heavy приложений
  • Один процесс, общая память

Минусы:

  • Не помогает для CPU-bound задач
  • Требует асинхронного кода (не совместимо с синхронными библиотеками)

3. PyPy, Jython, IronPython (альтернативные реализации)

# PyPy НЕ имеет GIL! Но совместимость с C расширениями ниже
# Jython работает на JVM и использует Java threading
# Использование: те же Python скрипты, другой интерпретатор

# pypy3 script.py  # Вместо python3 script.py

4. Numba JIT (для численных вычислений)

from numba import jit
import numpy as np

@jit(nopython=True, parallel=True)
def compute(array):
    """JIT скомпилируется в машинный код, парализм работает"""
    result = np.empty_like(array)
    for i in range(len(array)):
        result[i] = array[i] ** 2
    return result

# CPU-bound вычисления без GIL! Очень быстро
result = compute(np.arange(10_000_000))

Практический выбор

Тип работы          | Выбор              | Причина
-------------------------------------------------
I/O-bound (сеть)    | asyncio ИЛИ threading | GIL не блокирует I/O
I/O-bound (файлы)   | asyncio ИЛИ threading | GIL освобождается
CPU-bound           | multiprocessing    | Обходит GIL
Численные вычисления | NumPy + Numba      | Работают с массивами
Webserver           | asyncio (aiohttp)  | Много параллельных I/O
DataFrame processing | pandas + multicore | Использует NumPy C code

Пример: как выбрать правильный подход

import asyncio
import requests
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Pool

# Задача 1: Скачать 1000 файлов (I/O-bound)
# ✅ asyncio
async def download_files_async():
    tasks = [asyncio.create_task(fetch_async(url)) for url in urls]
    await asyncio.gather(*tasks)

# Задача 2: Обработать 1 млн чисел (CPU-bound)
# ✅ multiprocessing
def process_numbers_parallel():
    with Pool() as pool:
        results = pool.map(heavy_computation, large_data)

# Задача 3: Web server с много запросов (I/O-bound)
# ✅ asyncio + FastAPI
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def root():
    data = await fetch_from_db()
    return data

Заключение

Главная особенность параллельности в Python — GIL блокирует выполнение CPU-bound кода в многопоточной среде. Однако:

  • I/O-bound задачи: threading и asyncio работают отлично
  • CPU-bound задачи: используй multiprocessing или альтернативные реализации Python
  • Численные вычисления: NumPy и Numba обходят GIL

Понимание GIL критично для написания эффективного многопоточного Python кода.