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

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

2.0 Middle🔥 171 комментариев
#Python Core

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

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

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

# Почему несколько потоков не могут выполнять задачи параллельно в Python?

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

Что такое GIL (Global Interpreter Lock)?

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

import threading
import time

def worker(thread_id):
    """Функция, которую выполняет поток"""
    print(f"Поток {thread_id} начал работу")
    for i in range(100_000_000):
        pass  # Вычисления
    print(f"Поток {thread_id} завершил работу")

# Синхронное выполнение
start = time.time()
worker(1)
worker(2)
print(f"Синхронно: {time.time() - start:.2f} сек")

# С потоками (работает медленнее из-за GIL!)
start = time.time()
threads = [
    threading.Thread(target=worker, args=(1,)),
    threading.Thread(target=worker, args=(2,))
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"С потоками: {time.time() - start:.2f} сек")

# Результат:
# Синхронно: ~2.5 сек
# С потоками: ~2.5-3.0 сек (медленнее из-за overhead GIL!)

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

GIL был введен в CPython для упрощения управления памятью. Python использует подсчет ссылок для управления памятью:

import sys

obj = []
print(sys.getrefcount(obj))  # Количество ссылок на объект

def process(o):
    print(sys.getrefcount(o))

process(obj)  # Счетчик ссылок растет и уменьшается

Подсчет ссылок не потокобезопасен без синхронизации:

# Проблема без GIL:
# Поток 1: читает счетчик ссылок (8)
# Поток 2: читает счетчик ссылок (8)
# Поток 1: уменьшает счетчик (7)
# Поток 2: уменьшает счетчик (7)
# Результат: счетчик должен быть 6, но он 7!
# Объект не удалилась, когда должна была → утечка памяти

# С GIL:
# Только один поток может менять счетчик в одно время
# Операции атомарны

Практический пример проблемы

import threading
import time

# CPU-bound задача (вычисления)
def cpu_bound_task(n):
    """Выполняет вычисления — CPU-bound"""
    total = 0
    for i in range(n):
        total += i ** 2
    return total

# Одноядерное выполнение
start = time.time()
result1 = cpu_bound_task(100_000_000)
result2 = cpu_bound_task(100_000_000)
single_core_time = time.time() - start
print(f"Один поток: {single_core_time:.2f} сек")

# Многопоточное выполнение
start = time.time()
t1 = threading.Thread(target=cpu_bound_task, args=(100_000_000,))
t2 = threading.Thread(target=cpu_bound_task, args=(100_000_000,))
t1.start()
t2.start()
t1.join()
t2.join()
multi_thread_time = time.time() - start
print(f"Два потока: {multi_thread_time:.2f} сек (медленнее!)")
print(f"Замедление: {multi_thread_time / single_core_time:.1f}x")

# Результат:
# Один поток: ~2.5 сек
# Два потока: ~2.7-3.0 сек (медленнее из-за GIL!)
# Замедление: 1.1x

Когда потоки работают нормально: I/O-bound задачи

Потоки хорошо работают для операций, которые блокируют (ввод-вывод):

import threading
import requests
import time

# I/O-bound задача (сетевые запросы)
def fetch_url(url):
    """Получить контент с URL — I/O-bound"""
    response = requests.get(url)
    return len(response.content)

urls = [
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/2"
]

# Последовательное выполнение
start = time.time()
for url in urls:
    fetch_url(url)
sequential_time = time.time() - start
print(f"Последовательно: {sequential_time:.2f} сек")

# Многопоточное выполнение
start = time.time()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
    t.start()
for t in threads:
    t.join()
multithreaded_time = time.time() - start
print(f"Многопоточно: {multithreaded_time:.2f} сек (быстрее!)")
print(f"Ускорение: {sequential_time / multithreaded_time:.1f}x")

# Результат:
# Последовательно: 6.1 сек (2 сек * 3 запроса)
# Многопоточно: 2.1 сек (примерно одновременно)
# Ускорение: 3x

Решение 1: Используйте multiprocessing для CPU-bound

import multiprocessing
import time

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

if __name__ == '__main__':
    # Процессы имеют свои GIL, поэтому работают параллельно
    start = time.time()
    with multiprocessing.Pool(2) as pool:
        results = pool.map(cpu_bound_task, [100_000_000, 100_000_000])
    parallel_time = time.time() - start
    print(f"Два процесса: {parallel_time:.2f} сек (параллелизм!)")
    
    # На двухядерной машине это примерно в 2 раза быстрее
    # Каждый процесс имеет свой GIL

Решение 2: Используйте asyncio для I/O-bound

import asyncio
import aiohttp
import time

async def fetch_url_async(session, url):
    """Асинхронный запрос без потоков"""
    async with session.get(url) as response:
        return len(await response.content.read())

async def main():
    urls = [
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/2"
    ]
    
    # Асинхронное выполнение (не требует потоков)
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url_async(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    return results

# Запуск
start = time.time()
asyncio.run(main())
async_time = time.time() - start
print(f"Asyncio: {async_time:.2f} сек")

# Результат: ~2.1 сек (как многопоточно, но без GIL)

Решение 3: C расширения и numba

import numpy as np
from numba import jit
import threading
import time

# Обычный Python — медленно
def python_calculation(n):
    result = 0
    for i in range(n):
        result += i ** 2
    return result

# JIT компиляция с Numba — быстро и может использовать параллелизм
@jit(nopython=True, parallel=True)
def numba_calculation(n):
    result = 0
    for i in range(n):
        result += i ** 2
    return result

start = time.time()
python_calculation(100_000_000)
print(f"Python: {time.time() - start:.2f} сек")

start = time.time()
numba_calculation(100_000_000)
print(f"Numba (параллельный): {time.time() - start:.2f} сек")

# Результат: Numba может быть в 2-4x быстрее

Решение 4: Используйте альтернативные реализации Python

# PyPy — имеет GIL, но обычно быстрее для CPU-bound задач

# Jython — работает на JVM, нет GIL (но медленнее для чистого Python)

# IronPython — работает на .NET, нет GIL

# Cython — скомпилировать Python в C, убрать GIL
# cython
# def cpu_bound_task(n):
#     cdef int total = 0
#     cdef int i
#     for i in range(n):
#         total += i ** 2
#     return total

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

import threading
import multiprocessing
import asyncio

# ❌ CPU-bound + многопоточность
# Медленнее, чем однопоточность из-за overhead GIL
def cpu_bound_with_threads():
    threads = [threading.Thread(target=heavy_computation) for _ in range(4)]
    # ...

# ✅ CPU-bound + многопроцессность
# Быстро, параллельное выполнение на нескольких ядрах
def cpu_bound_with_processes():
    processes = [multiprocessing.Process(target=heavy_computation) for _ in range(4)]
    # ...

# ✅ I/O-bound + многопоточность
# Хорошо, потоки блокируются на I/O
def io_bound_with_threads():
    threads = [threading.Thread(target=fetch_data) for _ in range(10)]
    # ...

# ✅ I/O-bound + asyncio
# Отлично, эффективнее потоков
async def io_bound_with_asyncio():
    tasks = [fetch_data_async() for _ in range(10)]
    await asyncio.gather(*tasks)

Будущее Python: PEP 703 (Remove GIL)

В октябре 2023 года была принята PEP 703 о удалении GIL из Python. Guido van Rossum лично возглавляет этот проект:

# В будущем Python 3.13+ можно будет скомпилировать без GIL
# python3.13 --disable-gil script.py
# Или с флагом при компиляции CPython

# Это позволит многопоточности работать как ожидается
# Многопоточность будет такой же эффективной как multiprocessing

Заключение

Несколько потоков не работают параллельно в Python из-за GIL, который защищает внутреннее состояние интерпретатора.

Правило:

  • CPU-bound: используй multiprocessing (разные процессы, разные GIL)
  • I/O-bound: используй threading (потоки хороши) или лучше asyncio (экономнее)
  • Будущее: Python 3.13+ будет иметь опцию удалить GIL

Понимание GIL и его ограничений — критический навык для Python-разработчика, работающего с производительностью.

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