Конкурентность, параллелизм и GIL

⏱ 35 мин · 🧩 заданий: 5

Конкурентность и параллелизм

  • Конкурентность — умение вести несколько задач одновременно «в процессе». Повар ставит воду, пока она греется — режет овощи, потом помешивает соус. Повар один, но задачи продвигаются вместе.
  • Параллелизм — задачи выполняются буквально одновременно. Три повара на трёх плитах.

Конкурентность — про структуру программы (как не простаивать при ожидании), параллелизм — про исполнение (использовать несколько ядер).

Два вида нагрузки

I/O-bound CPU-bound
Что ограничивает ожидание: сеть, диск, БД, API вычисления процессора
Примеры веб-сервер, парсер сайтов, боты, микросервисы обработка изображений, ML, шифрование, симуляции
Процессор большую часть времени простаивает загружен на 100%
Что помогает asyncio, потоки процессы, C-расширения (numpy), другие языки
import time, asyncio

async def fake_request(i):
    await asyncio.sleep(0.3)               # «ждём сеть»: процессор свободен
    return i

async def main():
    start = time.perf_counter()
    for i in range(3):                     # последовательно: 3 × 0.3 с
        await fake_request(i)
    print(f"последовательно: {time.perf_counter() - start:.1f} с")

    start = time.perf_counter()
    await asyncio.gather(*(fake_request(i) for i in range(3)))   # конкурентно: ждём вместе
    print(f"конкурентно:     {time.perf_counter() - start:.1f} с")

asyncio.run(main())

Ожидание можно совместить. А вот вычисления — нет: если задачи считают, а не ждут, одно ядро не станет работать быстрее от того, что задачи чередуются.

Процессы, потоки, корутины

Процесс Поток (thread) Корутина (asyncio)
Память своя общая с потоками процесса общая
Кто переключает ОС ОС (вытесняющая многозадачность) сама программа на await (кооперативная)
Стоимость создания высокая (мс, мегабайты) средняя очень низкая (тысячи и миллионы)
Параллелизм на ядрах в CPython да нет (GIL) нет (один поток)
Главные риски обмен данными дорог гонки данных блокирующий вызов останавливает всех
Модуль multiprocessing, concurrent.futures.ProcessPoolExecutor threading, ThreadPoolExecutor asyncio

GIL

Global Interpreter Lock — мьютекс в CPython: в каждый момент времени байткод Python в процессе выполняет только один поток.

Зачем он нужен:

  • управление памятью CPython построено на подсчёте ссылок; без GIL каждое x = y требовало бы атомарных операций или блокировок — это замедлило бы однопоточный код;
  • C-расширения проще писать безопасно.

Что это значит на практике:

  • CPU-bound код на чистом Python потоками не ускоряется — потоки по очереди держат GIL.
  • I/O-bound код ускоряется: при ожидании сети, диска, time.sleep поток отпускает GIL.
  • Библиотеки на C (numpy, hashlib на больших данных, сжатие zlib) тоже отпускают GIL во время тяжёлых вычислений — их можно распараллеливать потоками.
  • Поток отдаёт GIL и принудительно — раз в 5 мс (sys.getswitchinterval()), чтобы другие потоки не голодали.
import threading, time

def count(n):
    while n > 0:
        n -= 1

N = 30_000_000
start = time.perf_counter()
count(N); count(N)
print(f"последовательно: {time.perf_counter() - start:.2f} с")   # ≈ 1.6 с

start = time.perf_counter()
t1 = threading.Thread(target=count, args=(N,))
t2 = threading.Thread(target=count, args=(N,))
t1.start(); t2.start(); t1.join(); t2.join()
print(f"два потока:      {time.perf_counter() - start:.2f} с")   # ≈ 1.6 с — не быстрее!

(В браузерном Python потоков нет, поэтому пример без кнопки запуска.)

Free-threaded Python

С Python 3.13 есть экспериментальная сборка без GIL (python3.13t, PEP 703), в 3.14 она официально поддерживается. Потоки в ней работают на нескольких ядрах параллельно, но часть C-расширений ещё не адаптирована, а однопоточный код немного медленнее. Пока стандартная сборка — с GIL, и на собеседованиях спрашивают именно о ней.

GIL не защищает ваши данные

GIL делает атомарными отдельные операции байткода, но не ваши составные операции. counter += 1 — это чтение, сложение и запись; поток могут прервать посередине. Общие изменяемые данные в потоках всё равно нужно защищать Lock.

Как выбрать

Задача ждёт ввод-вывод (сеть, БД, файлы)? ├─ да → много соединений (сотни+) или асинхронный фреймворк (FastAPI, aiohttp)? │ ├─ да → asyncio │ └─ нет → ThreadPoolExecutor (просто, работает с синхронными библиотеками) └─ нет, считает процессор ├─ numpy / pandas / C-библиотеки → они уже быстрые, часто сами многопоточны └─ чистый Python → ProcessPoolExecutor / multiprocessing (или очередь задач: Celery)

Опасности конкурентности

Проблема Что происходит Защита
Гонка данных (race condition) результат зависит от порядка переключений Lock, очереди, неизменяемые данные
Взаимоблокировка (deadlock) два потока ждут блокировки друг друга единый порядок захвата, таймауты
Голодание (starvation) задача никогда не получает ресурс справедливые очереди
Блокирующий вызов в asyncio time.sleep/requests в корутине останавливает весь цикл await asyncio.sleep, async-библиотеки, to_thread

Следующие уроки разбирают каждый инструмент на практике.

Вопрос на собеседовании

«Что такое GIL и как он влияет на многопоточность?» — Глобальная блокировка CPython, из-за которой одновременно выполняется только один поток с байткодом Python. Поэтому потоки не ускоряют CPU-bound код (для него — процессы), но полезны для I/O-bound задач, так как при ожидании ввода-вывода GIL освобождается. GIL нужен для простого и быстрого подсчёта ссылок. В 3.13+ есть экспериментальная сборка без GIL.

Итог

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

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Заполнить таблицу

Что лучше подходит для задачи в CPython (с GIL)?

asyncio / потокипроцессы
Скачать 1000 страниц по HTTP
Пересчитать хеши паролей bcrypt для миллиона записей
Веб-сервер, который в основном ждёт базу данных
Обработать 10 000 изображений (ресайз на чистом Python)
Читать сообщения из нескольких очередей и сокетов
Численная симуляция в чистом Python на всех ядрах
Отметьте ячейку — «+», пустая — «−».
Задание 2 Один вариант ответа

Что такое GIL?

Задание 3 Один вариант ответа

Почему потоки в CPython всё же ускоряют программы, которые скачивают файлы по сети?

Задание 4 Один вариант ответа

Чем конкурентность отличается от параллелизма?

Задание 5 Несколько вариантов

Что верно про процессы и потоки?

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