GIL, память и сборка мусора
Два вопроса про внутренности CPython: глобальная блокировка интерпретатора и то, как освобождается память.
GIL (Global Interpreter Lock) — глобальная блокировка интерпретатора CPython, из-за которой в один момент времени байткод Python исполняет только один поток.
Вопрос: что такое GIL и почему он мешает?
Чёткий ответ. GIL — это мьютекс, который не даёт двум потокам одновременно исполнять Python-байткод. Поэтому несколько потоков не ускоряют чисто вычислительные (CPU-bound) задачи: они делят одно ядро по очереди. Это особенность реализации CPython, а не языка как такового.
Важно: GIL не мешает задачам ожидания (I/O-bound) — пока поток ждёт сеть или диск, GIL отпускается, и другой поток работает. Поэтому для сетевых задач потоки полезны.
| Тип задачи | Что помогает |
| CPU-bound (вычисления) | multiprocessing — отдельные процессы, у каждого свой GIL |
| I/O-bound (сеть, диск) | threading или asyncio — потоки эффективны на ожидании |
Потоки против процессов
Потоки делят память и GIL — хороши для ожидания. Процессы изолированы, у каждого свой интерпретатор и свой GIL — значит, настоящий параллелизм на нескольких ядрах для вычислений.
# Иллюстрация распределения CPU-нагрузки между процессами (концептуально)
def cpu_task(n):
total = 0
for i in range(n):
total += i * i
return total
# 4 процесса считают независимо, каждый на своём ядре, свой GIL:
tasks = [10**6, 10**6, 10**6, 10**6]
results = [cpu_task(n) for n in tasks]
print("задач посчитано:", len(results))
print("результат одной:", results[0])
Вывод:
задач посчитано: 4 результат одной: 333332833333500000
В реальном коде эти четыре cpu_task запустили бы через multiprocessing.Pool — тогда они считались бы параллельно на разных ядрах. С потоками же из-за GIL они шли бы по очереди и ускорения бы не было.
Что отвечать про обход GIL
- CPU-bound →
multiprocessing(или нативные расширения C/numpy, отпускающие GIL). - I/O-bound →
threadingилиasyncio. - GIL есть в CPython; в Jython/IronPython его нет, а в новых версиях CPython развивают режим без GIL.
Как Python освобождает память: подсчёт ссылок
Второй частый вопрос про внутренности — управление памятью. Основной механизм CPython — подсчёт ссылок: у каждого объекта есть счётчик ссылок на него. Присваивание увеличивает счётчик, del или выход имени из области видимости — уменьшает. Как только счётчик доходит до нуля, объект удаляется немедленно.
import sys
a = [] # одна ссылка: a
b = a # теперь две ссылки
# getrefcount показывает на 1 больше — учитывает свой временный аргумент
print("ссылок после b = a:", sys.getrefcount(a) - 1)
del b # одну ссылку убрали
print("ссылок после del b:", sys.getrefcount(a) - 1)
Вывод:
ссылок после b = a: 2 ссылок после del b: 1
Сборщик мусора для циклических ссылок
Подсчёта ссылок мало, когда объекты ссылаются друг на друга по кругу: их счётчики не станут нулём, даже если снаружи на них никто не ссылается. Для таких циклов есть отдельный сборщик мусора (модуль gc).
import gc
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b # a -> b
b.ref = a # b -> a : цикл
del a
del b # внешних имён нет, но объекты держат друг друга
collected = gc.collect() # сборщик циклов находит и убирает их
print("в цикле собрано объектов:", collected >= 2)
Вывод:
в цикле собрано объектов: True
Итог
- GIL не даёт двум потокам одновременно исполнять Python-байткод: CPU-задачи ускоряют процессы (
multiprocessing), а для I/O полезны потоки/asyncio. - Память управляется подсчётом ссылок: объект удаляется сразу, как счётчик станет нулём.
- Циклические ссылки счётчик не ловит — их убирает отдельный сборщик
gc.