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

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

2.3 Middle🔥 122 комментариев
#Python

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

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

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

Параллельность в Python с учетом GIL

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

Что такое GIL

GIL защищает внутреннее состояние CPython: память, подсчет ссылок (refcounting), структуры данных. Без GIL каждая операция требовала бы собственной блокировки, что привело бы к меньшей производительности.

# GIL упрощает разработку интерпретатора
# но затрудняет многопоточное программирование
import threading

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

# Это НЕ будет работать параллельно из-за GIL
threads = []
for _ in range(4):
    t = threading.Thread(target=cpu_bound_task, args=(100_000_000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()  # Выполняется ПОСЛЕДОВАТЕЛЬНО, не параллельно

1. Многопроцессность (Multiprocessing) — лучший способ для CPU-bound задач

У каждого процесса свой Python интерпретатор и свой GIL. Это истинный параллелизм.

import multiprocessing
import time

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

if __name__ == __main__:  # ВАЖНО на Windows
    n_workers = 4
    tasks = [100_000_000] * n_workers
    
    # Вариант 1: Pool
    with multiprocessing.Pool(n_workers) as pool:
        results = pool.map(cpu_bound_task, tasks)
    
    print(f"Results: {sum(results)}")
    # Ускорение: ~4x на 4 ядрах

Преимущества:

  • Истинный параллелизм
  • Использует все ядра процессора
  • Изоляция процессов (безопасность)

Недостатки:

  • Overhead на создание процесса (~50ms)
  • Дорого передавать данные (pickling/unpickling)
  • Сложнее отлаживать

2. Асинхронность (Asyncio) — для I/O-bound задач

Asyncio НЕ обходит GIL, но использует то, что I/O операции отпускают GIL.

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    """GIL отпускается при I/O операции"""
    async with session.get(url) as response:
        return await response.text()  # GIL освобождается тут

async def main():
    urls = [https://example.com] * 100
    
    async with aiohttp.ClientSession() as session:
        # Все 100 запросов работают "одновременно"
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    return results

# Последовательно: 100 * 1 сек = 100 сек
# С asyncio: ~1 сек (все запросы параллельны)
start = time.time()
results = asyncio.run(main())
print(f"Time: {time.time() - start}s")

Когда использовать:

  • Сетевые запросы (HTTP, WebSocket)
  • Работа с БД
  • Файловые операции

3. Threading для I/O-bound задач

Треды отпускают GIL при I/O операциях, что позволяет другим тредам выполняться.

import threading
import requests
import time

def fetch_url(url, results, index):
    response = requests.get(url)  # GIL отпускается
    results[index] = len(response.text)

urls = [https://example.com] * 10
results = [None] * len(urls)
threads = []

start = time.time()
for i, url in enumerate(urls):
    t = threading.Thread(target=fetch_url, args=(url, results, i))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Time: {time.time() - start}s")  # ~2 сек вместо 10

Vs Asyncio:

  • Проще писать
  • Меньше boilerplate
  • Но менее эффективно (thread overhead)

4. Процессы для Data Science задач

Для работы с NumPy/Pandas многопроцессность особенно полезна:

import multiprocessing
import numpy as np
import pandas as pd

def process_chunk(chunk):
    """NumPy/Pandas операции работают в отдельном процессе"""
    return chunk.sum() * 2

def parallel_dataframe_processing(df, n_workers=4):
    chunks = np.array_split(df, n_workers)
    
    with multiprocessing.Pool(n_workers) as pool:
        results = pool.map(process_chunk, chunks)
    
    return sum(results)

# Пример
df = pd.DataFrame({value: range(1_000_000)})
result = parallel_dataframe_processing(df)
print(result)

5. ProcessPoolExecutor vs ThreadPoolExecutor

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

# CPU-bound: ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(cpu_task, data))

# I/O-bound: ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(io_task, urls))

6. Numpy/Pandas операции обходят GIL

Много операций в NumPy и Pandas написаны на C и отпускают GIL:

import numpy as np
import threading
import time

def numpy_work():
    # GIL отпускается для этого на всю длительность
    arr = np.random.randn(10_000_000)
    return np.sum(arr ** 2)

# Это МОЖЕТ работать параллельно благодаря отпусканию GIL
threads = [threading.Thread(target=numpy_work) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("NumPy часто работает параллельно несмотря на GIL")

7. Будущее: PEP 703 (No-GIL Python)

Python 3.13+ имеет experimental режим без GIL:

# Компилировать CPython без GIL
python --disable-gil

Это обещает истинный многопоточный параллелизм без переписывания кода.

Практическая стратегия

Тип задачиМетодСкоростьСложность
CPU-bound (ML, обработка данных)multiprocessing.PoolОтличнаяСредняя
I/O-bound (API, БД)asyncioОтличнаяНизкая
I/O-bound (простые)ThreadPoolExecutorХорошаяНизкая
Смешанныеmultiprocessing для CPU + asyncio для I/OОтличнаяВысокая
NumPy/PandasThreading (они отпускают GIL)ХорошаяНизкая

Примеры бенчмарков

# CPU-bound: 100M итераций
# Последовательно: 10 сек
# Threading: 10 сек (GIL)
# Multiprocessing: 2.5 сек (4 ядра)

# I/O-bound: 100 HTTP запросов
# Последовательно: 50 сек
# Threading: 5 сек
# Asyncio: 1.5 сек

Заключение

  1. Для CPU-bound (ML, математика): используй multiprocessing
  2. Для I/O-bound (HTTP, БД): используй asyncio
  3. Для простого I/O: ThreadPoolExecutor
  4. NumPy/Pandas: threading часто работает хорошо благодаря отпусканию GIL
  5. Профилируй! Не угадывай — измеряй с помощью cProfile или timeit

GIL — ограничение, но есть много способов с ним работать эффективно.

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