$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКак устроен GIL в Python: что он ограничивает и как это влияет на реальный код

Как устроен GIL в Python: что он ограничивает и как это влияет на реальный код

$ sudo teach IT
·16 сентября 2026 г.·11 мин·155
Как устроен GIL в Python: что он ограничивает и как это влияет на реальный код

Разберём, почему GIL мешает CPU-bound потокам, а для IO-bound иногда всё “выглядит как будто работает”. Поймём, когда выбирать потоки, процессы и asyncio, и как измерить эффект на вашей задаче.

Содержание
Что такое GIL и зачем он нуженGIL в терминах модели исполненияЧто GIL защищает на практикеЧто GIL ограничивает именно в CPU-bound задачахПочему CPU-bound в потоках почти не ускоряетсяФормула эффекта (интуитивная)Когда потоки “работают”: IO-bound и освобождение GILДве разные причины «почему кажется, что работает»Почему иногда потоки дают почти линейный ростПочему измерения важнее убеждений: GIL не виден “на глаз”Потоки, процессы и asyncio: как выбирать с учётом ограниченийПотоки (threading): когда они уместныПроцессы (multiprocessing): когда нужно CPUasyncio: когда нужна большая конкуруентность без потоковНаглядные эксперименты: как увидеть эффект GIL на вашем кодеЭксперимент 1: CPU-bound на чистом PythonЭксперимент 2: IO-bound с ожиданиемТипичные подводные камни в реальном коде1) “Я использую потоки — значит ускорится”2) “У меня I/O, значит потоки всегда лучше”3) CPU-bound прячется в неожиданном месте4) asyncio + CPU без await = блокировка event loop5) Переоценка числа потоковПрактическая стратегия: от “гипотезы” к решениюШаг 1. Разделите задачу на фазыШаг 2. Профилирование, а не только таймингиШаг 3. Сценарии сравненияШаг 4. Учитывайте стоимость обмена даннымиКак мягко применить GIL-ориентированное решение на вашем кодеЕсли вы пишете сетевой парсер/агрегаторЕсли вы делаете ETL на больших данныхЕсли вы делаете “быстрый прототип на потоках”Где GIL не является проблемой (и почему об этом важно помнить)Выводы

GIL (Global Interpreter Lock) — одна из тех тем, о которые годами ломают копья: одни считают его «тормозом», другие — прагматичной инженерной деталью, которая упрощает реализацию интерпретатора и защищает от сложностей управления памятью и объектов. Важно другое: GIL не просто «существует», он прямо влияет на то, как Python ведёт себя в многопоточности, особенно на задачах, упирающихся в CPU (CPU-bound). А на задачах, где большую часть времени занимает ожидание (IO-bound), эффект может быть менее заметным — и даже создавать иллюзию, что «потоки ускоряют».

В этой статье разберём, как именно устроен GIL на уровне концепции, что именно он ограничивает, почему иногда потоки «кажутся» полезными, и как на практике выбирать между потоками, процессами и asyncio. Закроем всё измерениями: как проверить, что именно ограничивает вашу конкретную программу, и как интерпретировать результаты.


Что такое GIL и зачем он нужен

GIL в терминах модели исполнения

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

Если упростить:

  • Поток может «взять» GIL и начать исполнять байткод.
  • Пока GIL удерживается, другие потоки не могут выполнять байткод (хотя их можно планировать ОС и они могут «ждать»).
  • Иногда интерпретатор отпускает GIL, чтобы дать шанс другим потокам или обработать системные события.

Что GIL защищает на практике

GIL тесно связан с тем, что в CPython управление состоянием объектов и внутренними структурами интерпретатора в значительной степени не рассчитано на одновременное выполнение нескольких потоков интерпретатором. Исторически GIL облегчал:

  • обеспечение потокобезопасности структур интерпретатора без сложного гранулированного locking на уровне каждого объекта;
  • предсказуемость и стабильность производительности интерпретатора.

Но ключевая мысль: даже если в вашем коде нет разделяемых структур данных, GIL ограничивает не «данные», а сам факт выполнения байткода.


Что GIL ограничивает именно в CPU-bound задачах

Почему CPU-bound в потоках почти не ускоряется

CPU-bound задача — это вычисления, где поток большую часть времени проводит, исполняя байткод и/или вычисляя в Python-коде. Например:

  • обработка больших массивов чисел на чистом Python;
  • парсинг/преобразования в циклах на Python-уровне;
  • сериализация/десериализация на стороне Python без освобождения GIL;
  • любые циклы, где 100% времени — выполнение кода интерпретатора.

В такой ситуации:

  1. Поток берёт GIL.
  2. Исполняет вычисления.
  3. Другие потоки остаются «в ожидании», потому что GIL занят.
  4. Если интерпретатор периодически отдаёт GIL, то переключения происходят ценой overhead, но параллельной работы на нескольких ядрах не возникает.

Итог: вы почти наверняка увидите, что производительность не растёт пропорционально количеству потоков. Иногда она даже падает из‑за переключений и накладных расходов планирования.

Формула эффекта (интуитивная)

Представьте, что из времени выполнения вашей задачи:

  • T1 — часть, где выполняется байткод (удерживается GIL),
  • Trelease — часть, где GIL может быть отпущен (например, при некоторых блокирующих системных вызовах или в C-коде, который явно отпускает GIL),
  • Toverhead — накладные расходы планирования потоков.

Для CPU-bound обычно T1 близко к общему времени, а Trelease и Toverhead — малая доля. Поэтому увеличение потоков не даёт прироста.


Когда потоки “работают”: IO-bound и освобождение GIL

Две разные причины «почему кажется, что работает»

IO-bound задачи часто состоят из работы двух типов:

  1. ожидание ввода-вывода (сеть, диск, сокеты, таймеры);
  2. небольшие вычисления для обработки полученных данных.

На ожиданиях ОС поток обычно блокируется в системном вызове. Пока поток ждёт, интерпретатор может:

  • не удерживать GIL дольше, чем нужно;
  • переключиться на другой поток, который тоже может ждать или обрабатывать данные.

Важный нюанс: освобождение GIL происходит не «магически» из-за того, что у вас I/O. Скорее, CPython/библиотеки реализуют ожидания через системные вызовы, а некоторые C-расширения явно отпускают GIL на время долгих операций. В таких случаях один поток может ждать сеть, второй в это время взять GIL и обработать данные.

Почему иногда потоки дают почти линейный рост

Если ваша программа:

  • значительную часть времени проводит в ожидании I/O,
  • и на это время GIL реально отпускается (или выполнение байткода в этот момент не требуется),

то потоки могут масштабироваться до разумного предела (например, ограниченного каналом/лимитом файловых дескрипторов/конкуренцией к ресурсу).

Но есть подводный камень: «кажется, что ускорилось» часто потому что вы скрыли латентность (latency) I/O, а не потому что CPython начал выполнять CPU вычисления параллельно.


Почему измерения важнее убеждений: GIL не виден “на глаз”

Одна из самых частых ошибок при разговоре о GIL — подменять анализ вопросом «потоки ускоряют?» вместо вопроса «какая доля времени уходит на байткод и удержание GIL?».

Проблема в том, что:

  • профиль по времени выполнения легко вводит в заблуждение без понимания того, что именно происходит под капотом;
  • библиотека может выполнять тяжёлую работу в C и при этом освобождать GIL;
  • ваша CPU-bound часть может быть не в Python-циклами, а в вызовах в C-расширения (где поведение другое).

Поэтому дальше — практический подход к измерению.


Потоки, процессы и asyncio: как выбирать с учётом ограничений

Потоки (threading): когда они уместны

Выбирайте потоки, если:

  • задача преимущественно IO-bound;
  • или ваша тяжелая часть вызывает C-код, который освобождает GIL (что часто бывает в числовых библиотеках, некоторых оптимизированных пакетах, файловых/сетевых стэках);
  • вам нужна простая модель параллельности без полноценного межпроцессного взаимодействия.

Типичные примеры:

  • загрузка/обработка множества HTTP-запросов;
  • конкурентная работа с несколькими сокетами;
  • параллельная запись/чтение из диска, если библиотека и драйверы позволяют скрывать задержки.

Процессы (multiprocessing): когда нужно CPU

Если задача CPU-bound на уровне Python-байткода, то процессы обычно выигрывают, потому что у каждого процесса свой интерпретатор и свой GIL. Вы получаете реальный параллелизм по ядрам.

Выбирайте multiprocessing, если:

  • вычисления происходят на Python-уровне и реально занимают CPU;
  • вам нужно масштабирование на несколько ядер;
  • вы готовы принять накладные расходы межпроцессного взаимодействия и сериализации данных.

Подводные камни:

  • стоимость передачи данных между процессами (pickle/IPC);
  • рост памяти (каждый процесс имеет отдельное пространство);
  • сложность отладки и управления жизненным циклом.

asyncio: когда нужна большая конкуруентность без потоков

asyncio — это не «магия для CPU», это модель кооперативной конкуренции для I/O.

Выбирайте asyncio, если:

  • у вас много одновременных ожиданий (сеть, таймеры, очереди, async-IO библиотеки);
  • CPU-работа минимальна или умеет эффективно освобождать управление (например, кусочками с await, либо вынесена в отдельные worker-процессы/потоки).

Ключевой принцип: если внутри async-функции вы делаете долгие CPU вычисления без await, вы блокируете event loop и вся «конкурентность» превращается в последовательность.

Обычно в реальных проектах сочетают:

  • asyncio для I/O,
  • и либо процессы/пулы для CPU-bound,
  • либо оптимизированные библиотеки для тяжелых вычислений в C.

Наглядные эксперименты: как увидеть эффект GIL на вашем коде

Эксперимент 1: CPU-bound на чистом Python

Рассмотрим простую вычислительную функцию. Это не «лучший способ считать», это способ сделать CPU-bound задачу на Python-байткоде.

code
import time

def cpu_work(n: int) -> int:
    s = 0
    for i in range(n):
        s += (i * i) % 97
    return s

def run_single(tasks, n):
    t0 = time.perf_counter()
    results = [cpu_work(n) for _ in range(tasks)]
    dt = time.perf_counter() - t0
    return dt, results

if __name__ == "__main__":
    dt, _ = run_single(tasks=4, n=5_000_00)
    print(f"single-process time: {dt:.3f}s")

Теперь сравним потоки (threading) и процессы (multiprocessing). Для потоков:

code
from concurrent.futures import ThreadPoolExecutor
import time

def run_threads(workers, tasks, n):
    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=workers) as ex:
        results = list(ex.map(lambda _: cpu_work(n), range(tasks)))
    dt = time.perf_counter() - t0
    return dt, results

Для процессов:

code
from concurrent.futures import ProcessPoolExecutor
import time
import os

def cpu_work_wrapper(n):
    return cpu_work(n)

def run_processes(workers, tasks, n):
    t0 = time.perf_counter()
    with ProcessPoolExecutor(max_workers=workers) as ex:
        results = list(ex.map(cpu_work_wrapper, [n] * tasks))
    dt = time.perf_counter() - t0
    return dt, results

Ожидаемая картина:

  • threads: близко к single-process (иногда чуть хуже);
  • processes: рост примерно с числом ядер до момента, когда ресурсы (CPU/память) начнут упираться и станет доминировать overhead.

Важно: абсолютные цифры зависят от размера n, количества задач и конкретной машины, но качественный тренд для CPU-bound на чистом Python обычно стабилен.

Эксперимент 2: IO-bound с ожиданием

Теперь задача, где поток большую часть времени «спит» на I/O. Условно — чтение из сети. Для демонстрации используем time.sleep как имитацию блокировки:

code
import time

def io_work(delay: float) -> int:
    time.sleep(delay)
    return 1

Последовательный запуск:

code
from concurrent.futures import ThreadPoolExecutor
import time

def run_sequential(tasks, delay):
    t0 = time.perf_counter()
    results = [io_work(delay) for _ in range(tasks)]
    dt = time.perf_counter() - t0
    return dt, results

Потоки:

code
def run_threads_io(workers, tasks, delay):
    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=workers) as ex:
        results = list(ex.map(lambda _: io_work(delay), range(tasks)))
    dt = time.perf_counter() - t0
    return dt, results

Обычно потоки дадут заметный выигрыш, потому что параллелизм здесь про ожидание, а не про вычисления.

Но реальная проверка должна быть с настоящим I/O и вашими библиотеками: некоторые операции могут не отпускать GIL так, как ожидается, либо сериализация/обработка данных начнет доминировать.


Типичные подводные камни в реальном коде

1) “Я использую потоки — значит ускорится”

Не всегда. Если внутри потоков — CPU-bound байткод, GIL превратит потоки в «очередь переключений».

2) “У меня I/O, значит потоки всегда лучше”

Тоже нет. Если вы после I/O делаете тяжёлые преобразования на Python-уровне, они начнут конкурировать за GIL и съедят выигрыш.

3) CPU-bound прячется в неожиданном месте

Часто CPU растёт в:

  • декодировании/парсинге,
  • сериализации JSON,
  • сжатии/распаковке,
  • построении больших структур данных.

Даже если сетевое получение медленное, вы можете упрётся в обработку.

4) asyncio + CPU без await = блокировка event loop

Если в async-обработчике вы делаете длительный CPU расчёт, другие корутины будут простаивать. В таком случае:

  • выносят CPU в процессы/пул потоков,
  • либо используют оптимизированные библиотеки, которые выполняют работу в C и могут освобождать GIL.

5) Переоценка числа потоков

Потоки — это не бесплатный ресурс. Слишком много потоков:

  • усиливают contention за GIL,
  • создают overhead переключений,
  • увеличивают давление на планировщик ОС.

В IO-bound кейсах число потоков часто ограничивают практическими лимитами: размер пула, лимиты соединений, пропускная способность сети/диска.


Практическая стратегия: от “гипотезы” к решению

Шаг 1. Разделите задачу на фазы

В вашем пайплайне примерно так:

  • ожидание (I/O),
  • обработка (CPU),
  • упаковка/распаковка данных (часто и CPU, и память),
  • запись результата (I/O).

Дальше вам нужно оценить, где доминирует время.

Шаг 2. Профилирование, а не только тайминги

Минимально полезно:

  • измерить wall-clock время,
  • посмотреть CPU time (например, через системные инструменты),
  • при необходимости — включить профайлер (cProfile/py-spy/scalene) и посмотреть горячие места.

Если горячие функции — циклы на Python, то почти наверняка потоки не дадут масштабирования.

Шаг 3. Сценарии сравнения

Проведите A/B:

  • single-thread/single-process baseline;
  • threads (несколько десятков) для I/O-сценария;
  • processes (по числу ядер) для CPU-сценария;
  • asyncio (для I/O с корутинами) и добавьте отдельный канал для CPU (process pool).

Не пытайтесь выбрать решение «по ощущению». Лучше сравнить 2–3 варианта на репрезентативных данных.

Шаг 4. Учитывайте стоимость обмена данными

В multiprocessing часто основной «скрытый» расход — сериализация. Если у вас большие объекты, возможно выгоднее:

  • уменьшить размер пересылаемых данных,
  • использовать shared memory (где применимо),
  • перенести обработку ближе к данным,
  • или перейти на thread pool там, где тяжелое ядро выполняется в C и GIL отпускается.

Как мягко применить GIL-ориентированное решение на вашем коде

Если вы пишете сетевой парсер/агрегатор

Частый паттерн:

  • asyncio для одновременных запросов,
  • отдельные worker’ы (процессы) для тяжелого парсинга/обработки,
  • или использование быстрых библиотек (ujson/orjson, lxml с правильными режимами и т.п., если они выполняют работу в C).

Если вы делаете ETL на больших данных

  • Чистый Python CPU-bound циклы: скорее процессы или внешние оптимизации (векторизация/NumPy/Numba/Cython).
  • Смешанные этапы: архитектурно разнесите I/O и CPU по разным слоям: асинхронный сбор входных данных и параллельная обработка на worker-пуле.

Если вы делаете “быстрый прототип на потоках”

Прототип можно. Но если цель — реальное ускорение, заранее заложите возможность смены реализации:

  • тестируйте на CPU-bound нагрузках,
  • не полагайтесь на то, что «потоки просто помогут».

Где GIL не является проблемой (и почему об этом важно помнить)

GIL — не универсальная причина медленности. Он ограничивает параллельное выполнение байткода в CPython. Но реальная программа может оказаться:

  • не CPU-bound в Python-уровне,
  • или большую часть времени проводить в C-коде библиотек,
  • или быть ограниченной внешними ресурсами: сеть, диск, база данных.

Тогда потоки/asyncio действительно дают прирост, и GIL становится «не самым узким местом». Именно поэтому важно измерять конкретную задачу.


Выводы

GIL в CPython ограничивает параллельное выполнение байткода в нескольких потоках одновременно. Поэтому для CPU-bound задач, где вычисления идут на Python-уровне, потоки почти всегда не ускоряют (а иногда замедляют из‑за overhead). Для IO-bound задач потоки могут выглядеть эффективными — потому что во время ожидания I/O интерпретатор и/или C-реализации библиотек могут отпускать GIL, позволяя другим потокам продвигаться.

Практический выбор выглядит так:

  • Потоки — преимущественно для I/O и для случаев, где тяжёлая работа находится в C-коде и освобождает GIL.
  • Процессы — если CPU-bound работа действительно выполняется на Python-байткоде и нужно масштабирование по ядрам.
  • asyncio — удобная модель конкурентности для I/O; CPU-работу нужно либо оптимизировать, либо выносить в процессы/пулы.

Если хотите глубже разобраться именно в механике многопоточности/параллелизма и том, как это влияет на архитектуру Python-приложений, полезной отправной точкой может стать курс по теме, например «»: он может помочь структурировать понимание и увидеть типовые паттерны принятия решений. Но в любом случае главный инструмент остаётся один — измерять ваш код на реальной нагрузке и проверять, где именно находится узкое место.


Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Протоколы интернета - продвинутый курс

Протоколы интернета - продвинутый курс

Что происходит под капотом интернета: TCP и QUIC на уровне полей и состояний, NAT и прямые соединения, туннели и VPN-протоколы, прокси, анонимные и децентрализованные сети, приватность и разборы реальных взломов. 19 модулей, 133 темы, 19 тестов.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Протоколы интернета - простыми словами

Протоколы интернета - простыми словами

Как данные находят дорогу от вашего устройства до сайта. IP, UDP, TCP, DHCP, DNS, HTTP, TLS и HTTPS - на бытовых аналогиях, без жаргона, формул и командной строки. 10 модулей, 55 уроков, 10 тестов.

Перейти к курсу

Другие статьи

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1770
Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1360
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1940
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
90
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1930
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

Разберём, что такое базы данных и SQL без сложной теории: как выбирать данные, фильтровать их и собирать таблицы в отчёт. В конце — короткий план, как потренироваться и собрать первые запросы для портфолио.

28 октября 2026 г.
210

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Что такое GIL и зачем он нуженGIL в терминах модели исполненияЧто GIL защищает на практикеЧто GIL ограничивает именно в CPU-bound задачахПочему CPU-bound в потоках почти не ускоряетсяФормула эффекта (интуитивная)Когда потоки “работают”: IO-bound и освобождение GILДве разные причины «почему кажется, что работает»Почему иногда потоки дают почти линейный ростПочему измерения важнее убеждений: GIL не виден “на глаз”Потоки, процессы и asyncio: как выбирать с учётом ограниченийПотоки (threading): когда они уместныПроцессы (multiprocessing): когда нужно CPUasyncio: когда нужна большая конкуруентность без потоковНаглядные эксперименты: как увидеть эффект GIL на вашем кодеЭксперимент 1: CPU-bound на чистом PythonЭксперимент 2: IO-bound с ожиданиемТипичные подводные камни в реальном коде1) “Я использую потоки — значит ускорится”2) “У меня I/O, значит потоки всегда лучше”3) CPU-bound прячется в неожиданном месте4) asyncio + CPU без await = блокировка event loop5) Переоценка числа потоковПрактическая стратегия: от “гипотезы” к решениюШаг 1. Разделите задачу на фазыШаг 2. Профилирование, а не только таймингиШаг 3. Сценарии сравненияШаг 4. Учитывайте стоимость обмена даннымиКак мягко применить GIL-ориентированное решение на вашем кодеЕсли вы пишете сетевой парсер/агрегаторЕсли вы делаете ETL на больших данныхЕсли вы делаете “быстрый прототип на потоках”Где GIL не является проблемой (и почему об этом важно помнить)Выводы