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

Почему для вычислительных операций в Python не подходит многопоточность?

1.0 Junior🔥 161 комментариев
#Асинхронность и многопоточность

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

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

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

GIL (Global Interpreter Lock) и многопоточность в Python

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

Что такое GIL?

GIL — это мьютекс (взаимное исключение), который защищает доступ к объектам в памяти CPython. Правило простое:

В любой момент времени только ОДИН поток может выполнять Python код.

Даже если у вас есть многоядерный процессор, только один поток может работать с байт-кодом Python.

import threading
import time

def cpu_bound_work(n):
    """Вычислительная операция, привязанная к CPU"""
    result = 0
    for i in range(n):
        result += i ** 2
    return result

# Однопоточное выполнение
start = time.time()
for _ in range(4):
    cpu_bound_work(50_000_000)
time_single = time.time() - start
print(f"Однопоточное: {time_single:.2f} сек")  # ~4.2 сек

# Четырёхпоточное выполнение
start = time.time()
threads = []
for _ in range(4):
    t = threading.Thread(target=cpu_bound_work, args=(50_000_000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

time_multi = time.time() - start
print(f"Четырёхпоточное: {time_multi:.2f} сек")  # ~4.5 сек! (МЕДЛЕННЕЕ!)
print(f"Ускорение: {time_single / time_multi:.2f}x")  # 0.93x (замедление!)

Почему это происходит?

1. Context Switching (переключение контекста)

# GIL вынужден переключаться между потоками
# Даже на многоядерном процессоре

thread_1: [Python code] -> [release GIL] -> [waiting]
                              ^
                              |
                         (context switch)
                              |
                              v
thread_2:                         [acquire GIL] -> [Python code] -> [release GIL]

# Context switching стоит дорого:
# - Сохранение состояния процессора
# - Загрузка состояния другого потока
# - Инвалидация кэша процессора

2. GIL конкурирует за процессорное время

Даже если потоки работают на разных ядрах, GIL не позволяет одновременное выполнение:

from threading import Thread, Lock
import time

# Симуляция работы GIL
class GIL:
    def __init__(self):
        self.owner = None
        self.lock = Lock()
    
    def acquire(self, thread_id):
        with self.lock:
            self.owner = thread_id
    
    def release(self):
        self.owner = None

gil = GIL()

def worker(thread_id):
    for i in range(100):
        gil.acquire(thread_id)
        # Выполнение Python кода
        sum(range(10000))
        gil.release()
        # Если GIL занят другим потоком, текущий ждёт

start = time.time()
threads = [Thread(target=worker, args=(i,)) for i in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Время с 4 потоками: {time.time() - start:.2f} сек")

Визуальное объяснение

# На ОДНОПОТОЧНОМ процессоре
Core 0: [Thread 1] -> [GIL switch] -> [Thread 2] -> [GIL switch] -> [Thread 1]
Медленно: потоки сериализуются

# На МНОГОЯДЕРНОМ процессоре (ошибочное предположение)
Core 0: [Thread 1] ################
Core 1:                            [Thread 2] ################
Core 2: [idle]
Core 3: [idle]

# РЕАЛЬНОСТЬ с GIL
Core 0: [Thread 1] #### [switch] [Thread 2] #### [switch] [Thread 1]
Core 1: [idle - GIL занят потоком на Core 0]
Core 2: [idle - GIL занят потоком на Core 0]
Core 3: [idle - GIL занят потоком на Core 0]

Все потоки борются за ОДИН GIL, только один может выполняться

Практический пример: CPU-bound vs I/O-bound

I/O-bound операции (многопоточность РАБОТАЕТ!)

import threading
import requests
import time

def fetch_url(url):
    """I/O-bound операция (сетевой запрос)"""
    try:
        response = requests.get(url, timeout=5)
        return response.status_code
    except:
        return None

# Однопоточное
start = time.time()
urls = [
    'https://example.com',
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/2',
]
for url in urls:
    fetch_url(url)
time_single = time.time() - start
print(f"Однопоточное: {time_single:.2f} сек")  # ~8 сек

# Четырёхпоточное
start = time.time()
threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
time_multi = time.time() - start
print(f"Четырёхпоточное: {time_multi:.2f} сек")  # ~2 сек (БЫСТРЕЕ!)
print(f"Ускорение: {time_single / time_multi:.2f}x")  # 4x

# Почему? Пока один поток ждёт ответа от сервера (I/O),
# другие потоки могут выполняться. GIL отпускается при I/O.

CPU-bound операции (многопоточность НЕ работает)

import threading
import time

def cpu_work(n):
    """CPU-bound операция"""
    result = 0
    for i in range(n):
        result += i ** 2
    return result

# GIL НЕ отпускается, потому что это чистые вычисления в Python
# Потоки всё время конкурируют за GIL

Решения для CPU-bound операций

1. multiprocessing (нужные правильный выход)

from multiprocessing import Pool, cpu_count
import time

def cpu_work(n):
    result = 0
    for i in range(n):
        result += i ** 2
    return result

# multiprocessing создаёт отдельные процессы, каждый с собственным GIL
if __name__ == '__main__':
    start = time.time()
    
    # Используем все доступные процессоры
    with Pool(cpu_count()) as pool:
        results = pool.map(cpu_work, [50_000_000] * 4)
    
    time_multi = time.time() - start
    print(f"Multiprocessing: {time_multi:.2f} сек")  # ~1.2 сек (4x ускорение!)

2. asyncio (для асинхронных операций)

import asyncio
import aiohttp

async def fetch_urls():
    """Асинхронное выполнение I/O операций"""
    async with aiohttp.ClientSession() as session:
        tasks = [
            session.get('https://example.com'),
            session.get('https://example.com'),
            session.get('https://example.com'),
            session.get('https://example.com'),
        ]
        responses = await asyncio.gather(*tasks)
        return [r.status for r in responses]

# asyncio работает в одном потоке, но эффективнее благодаря event loop
# Лучше для I/O-bound операций

3. Расширения на C (NumPy, Cython)

import numpy as np
import time
from multiprocessing import Pool

# NumPy отпускает GIL при выполнении операций на C уровне
arr = np.arange(50_000_000)

start = time.time()
for _ in range(4):
    result = np.sum(arr ** 2)
print(f"NumPy: {time.time() - start:.2f} сек")  # ~0.2 сек

# NumPy автоматически распределяет работу на несколько потоков
# и отпускает GIL, потому что вычисления происходят на C уровне

4. Cython

# compute.pyx
def cpu_work_cython(int n):
    cdef int result = 0
    cdef int i
    for i in range(n):
        result += i * i
    return result

# После компиляции можно использовать `nogil` для отпуска GIL

cdef int cpu_work_nogil(int n) nogil:  # nogil позволяет отпустить GIL
    cdef int result = 0
    cdef int i
    for i in range(n):
        result += i * i
    return result

Когда GIL отпускается?

import threading
import time

# GIL отпускается при:
# 1. I/O операциях (файлы, сеть, БД)
# 2. Операциях в расширениях на C (NumPy, Pandas, Pillow)
# 3. Использовании time.sleep()

class GILBehavior:
    @staticmethod
    def io_operation():
        """GIL отпускается"""
        with open('file.txt', 'r') as f:
            data = f.read()
        # Во время чтения файла другие потоки могут выполняться
    
    @staticmethod
    def cpu_operation():
        """GIL НЕ отпускается"""
        result = sum(i ** 2 for i in range(10_000_000))
        # Только этот поток может выполняться
    
    @staticmethod
    def sleep():
        """GIL отпускается"""
        time.sleep(1)
        # Во время sleep другие потоки могут выполняться

Резюме

СценарийМногопоточностьРекомендация
CPU-boundНе работаетmultiprocessing или NumPy/Cython
I/O-bound (блокирующее)Работает хорошоthreading
I/O-bound (асинхронное)Работает отличноasyncio
СмешанноеЗависитmultiprocessing + asyncio

Ключевые выводы

  1. GIL существует — защищает внутренние структуры данных CPython
  2. Для CPU-bound операций многопоточность бесполезна — используй multiprocessing
  3. Для I/O-bound операций многопоточность работает — потоки могут выполняться, пока один ждёт I/O
  4. NumPy и другие расширения отпускают GIL — эффективны для параллельных вычислений
  5. asyncio — правильный выбор для I/O — асинхронное выполнение в одном потоке
  6. Python 3.13+ улучшает ситуацию — экспериментальная поддержка "free-threaded" режима

Выбор инструмента зависит от типа вашей задачи!

Почему для вычислительных операций в Python не подходит многопоточность? | PrepBro