Конкурентность, параллелизм и GIL
Конкурентность и параллелизм
- Конкурентность — умение вести несколько задач одновременно «в процессе». Повар ставит воду, пока она греется — режет овощи, потом помешивает соус. Повар один, но задачи продвигаются вместе.
- Параллелизм — задачи выполняются буквально одновременно. Три повара на трёх плитах.
Конкурентность — про структуру программы (как не простаивать при ожидании), параллелизм — про исполнение (использовать несколько ядер).
Два вида нагрузки
| 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.
Как выбрать
Опасности конкурентности
| Проблема | Что происходит | Защита |
|---|---|---|
| Гонка данных (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
Что лучше подходит для задачи в CPython (с GIL)?
| asyncio / потоки | процессы | |
|---|---|---|
| Скачать 1000 страниц по HTTP | ||
| Пересчитать хеши паролей bcrypt для миллиона записей | ||
| Веб-сервер, который в основном ждёт базу данных | ||
| Обработать 10 000 изображений (ресайз на чистом Python) | ||
| Читать сообщения из нескольких очередей и сокетов | ||
| Численная симуляция в чистом Python на всех ядрах |
Что такое GIL?
Почему потоки в CPython всё же ускоряют программы, которые скачивают файлы по сети?
Чем конкурентность отличается от параллелизма?
Что верно про процессы и потоки?
Войдите, чтобы отмечать пройденные уроки.