сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Python: ядро языка

Типизация, GIL, окружение

Зачем это спрашивают

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

// Ответ на любой вопрос о конкурентности начинается с классификации задачи: CPU-bound или I/O-bound.

Аннотации: типы без рантайма

Аннотации типов в рантайме не проверяются - их читают статический чекер (mypy/pyright) и IDE. Без чекера в CI аннотация - просто документация, которая имеет право врать.

Рабочий словарь: X | None (бывший Optional), list[str], dict[str, int], Callable, TypeVar для обобщений, Protocol - типизированная утиная типизация: «подходит всё, у чего есть такие методы».

// Виртуальное окружение на проект плюс lock-файл зависимостей (uv/poetry/pip-tools) - ровно то, чем лечится «у меня работает».

Protocol
структурная типизация: важен набор методов, а не наследование

GIL: почему потоки не про скорость вычислений

GIL (global interpreter lock) - глобальный лок интерпретатора: в одном процессе CPython байткод исполняет только один поток в каждый момент. Потоки не ускоряют CPU-bound код вовсе.

Но для I/O потоки работают: на ожидании сети или диска GIL отпускается, и другие потоки бегут. Поэтому «потоки для ожидания, процессы для вычислений».

// CPU-bound параллелится multiprocessing/ProcessPoolExecutor - обход GIL ценой сериализации данных между процессами. Либо выход в numpy/C-расширения, где GIL отпускается внутри.

GIL
один поток исполняет байткод в момент времени; отпускается на I/O
CPU-bound / I/O-bound
узкое место - вычисления / ожидание внешних операций

asyncio и выбор инструмента

asyncio - конкурентность на одном потоке через event loop: тысячи одновременных I/O-задач без потоков. Цена дисциплины: любой блокирующий вызов - requests, time.sleep - морозит весь loop, нужны async-аналоги.

Шпаргалка выбора: I/O-bound с массой соединений - asyncio; I/O-bound скромный - потоки; CPU-bound - процессы или numpy.

// Гонки никуда не делись и с GIL: «dict же потокобезопасный» - отдельные операции да, но проверил-и-записал из двух потоков - уже не атомарно.

event loop
цикл, переключающий задачи на точках await; один поток

Как отвечать: «Распараллелил обработку потоками - быстрее не стало. Почему?»

Похоже, задача CPU-bound: из-за GIL байткод в процессе исполняет один поток за раз, потоки дают выигрыш только там, где есть ожидание I/O - на нём GIL отпускается. Для числодробилки беру ProcessPoolExecutor - отдельные процессы обходят GIL, помня, что данные между ними сериализуются, либо переписываю горячее место на numpy/векторизацию, где GIL отпускается в C-коде. А если задача всё-таки I/O - проверяю, не сидит ли всё на одном общем локе или блокирующем вызове.

Диагноз через классификацию задачи, два лечения с их ценой и запасная ветка - структура ответа человека, который в это уже упирался.

На чём валят

  • Потоки для ускорения числодробилки - GIL, ускорения нет.
  • requests или time.sleep внутри async def - event loop встал целиком.
  • Пакеты в системный питон без venv - конфликты версий между проектами.
  • Верить аннотации без чекера - в рантайме она ничего не гарантирует.
  • «dict потокобезопасный» - многошаговые операции из потоков не атомарны.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #typing_env_concurrency1 / 5
    Что такое GIL и почему для CPU-bound задач берут процессы, а не потоки?
    A)GIL как раз позволяет нескольким потокам Python по-настоящему параллельно исполнять байт-код
    B)GIL пускает лишь один поток исполнять байт-код разом; CPU-bound масштабируют процессами
    C)GIL — это ограничение на объём памяти, доступной одному потоку Python
    D)Для CPU-bound задач в Python потоки быстрее отдельных процессов
    показать ответ и разбор
    +B)GIL пускает лишь один поток исполнять байт-код разом; CPU-bound масштабируют процессами

    // разбор: GIL (global interpreter lock) — блокировка, из-за которой в один момент байт-код Python исполняет только один поток. Поэтому многопоточность не ускоряет CPU-bound задачи (потоки упираются в GIL) — их масштабируют процессами (multiprocessing) или уносят в нативный код (numpy, C-расширения). Для I/O-bound задач потоки полезны: во время ожидания ввода-вывода GIL отпускается.

  2. #typing_env_concurrency2 / 5
    Сервис делает много медленных сетевых запросов (I/O-bound). Что уместнее — потоки, процессы или asyncio, и почему?
    A)Процессы (multiprocessing): для параллельных задач в Python по-настоящему подходят они одни
    B)Ничего из перечисленного не поможет: из-за GIL интерпретатор Python не способен обрабатывать несколько операций конкурентно вообще никак
    C)Потоки или asyncio: на I/O-ожидании GIL освобождается, поэтому конкурентность работает; asyncio дёшево масштабирует тысячи соединений
    D)Процессы, потому что и asyncio, и потоки в Python работают последовательно и конкурентности не дают
    показать ответ и разбор
    +C)Потоки или asyncio: на I/O-ожидании GIL освобождается, поэтому конкурентность работает; asyncio дёшево масштабирует тысячи соединений

    // разбор: GIL мешает лишь CPU-bound потокам (одновременно исполняется один байткод), но на I/O-ожидании (сеть, диск) GIL освобождается, и потоки реально перекрываются. Для множества медленных запросов подходят и потоки, и asyncio; asyncio на одном потоке через event loop дёшево держит тысячи одновременных соединений (меньше накладных расходов, чем нити). Процессы нужны для CPU-bound работы (обойти GIL), а здесь узкое место — ожидание I/O, а не вычисления.

  3. #typing_env_concurrency3 / 5
    CPU-тяжёлую функцию распараллелили через threading на 4 потока — быстрее не стало. Почему?
    A)потоки в Python «зелёные» — операционная система о них не знает
    B)GIL пускает к байткоду один поток за раз
    C)потоки не разделяют память — всё съело копирование данных
    D)потоков должно быть больше, чем ядер процессора
    показать ответ и разбор
    +B)GIL пускает к байткоду один поток за раз

    // разбор: GIL (global interpreter lock) в CPython позволяет исполнять байткод только одному потоку одновременно, поэтому threading ускоряет лишь ожидание I/O (сеть, диск), где GIL отпускается. CPU-bound параллелят процессами (multiprocessing) или уносят в нативный код (numpy, C-расширения), отпускающий GIL. Free-threaded-сборка без GIL существует, но пока не дефолт.

  4. #typing_env_concurrency4 / 5
    Внутри asyncio-корутины вызвали time.sleep(5). Что станет с остальными задачами event loop?
    A)Ничего: остальные задачи продолжат работать параллельно
    B)Event loop поднимет их в отдельных потоках
    C)Python бросит RuntimeError о блокирующем вызове
    D)Они встанут: синхронный sleep блокирует весь цикл
    показать ответ и разбор
    +D)Они встанут: синхронный sleep блокирует весь цикл

    // разбор: Event loop живёт в одном потоке. Кооперативная многозадачность работает, пока корутина отдаёт управление через await. time.sleep управление не отдаёт: он держит поток пять секунд, и ни одна другая задача за это время не сдвинется. Внутри async-кода спят через asyncio.sleep, а неизбежный блокирующий вызов уносят в пул через run_in_executor.

  5. #typing_env_concurrency5 / 5
    Проверяются ли аннотации типов в Python во время выполнения?
    A)Да: интерпретатор бросит TypeError при несоответствии
    B)Да, но только для аргументов функций, не для переменных
    C)Нет: они хранятся как метаданные, проверяет статический анализатор
    D)Нет, но pydantic делает их обязательными для всего кода
    показать ответ и разбор
    +C)Нет: они хранятся как метаданные, проверяет статический анализатор

    // разбор: Объявление def f(x: int) -> str не мешает передать список: интерпретатор кладёт аннотации в __annotations__ и идёт дальше. Несоответствия ловит mypy или pyright на этапе проверки. Рантайм-валидация — отдельный слой: pydantic и dataclasses с валидаторами читают те же аннотации и уже сами проверяют данные на границе сервиса.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.