← Назад к вопросам
Как задействован GIL при уничтожении объектов в Python?
2.2 Middle🔥 231 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
GIL и уничтожение объектов в Python
Это глубокий вопрос о том, как Global Interpreter Lock взаимодействует с механизмом подсчёта ссылок при удалении объектов. Разберусь в деталях.
Как работает уничтожение объектов
В Python используется подсчёт ссылок (reference counting) для управления памятью.
import sys
class MyObject:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"Удаляю объект {self.name}")
# Создаём объект
obj = MyObject("test")
print(f"Ref count: {sys.getrefcount(obj)}") # 2 (сам obj + аргумент в getrefcount)
# Удаляем последнюю ссылку
del obj # Вызывается __del__
Когда счётчик ссылок обнуляется, вызывается __del__ и объект удаляется.
Роль GIL при уничтожении
1. GIL защищает счётчик ссылок
Счётчик ссылок сам по себе не потокобезопасен. GIL защищает его от race conditions:
import threading
import sys
class Resource:
def __init__(self, name):
self.name = name
self.data = [0] * 1000000 # большой объект
def __del__(self):
print(f"Cleaning up {self.name}")
# БЕЗ GIL это было бы очень опасно:
# Поток 1: читает refcount = 2
# Поток 2: уменьшает refcount на 1 (теперь 1)
# Поток 1: уменьшает refcount (становится -1 или 0)
# -> Двойное удаление или утечка памяти!
# С GIL:
# Только один поток может изменять refcount одновременно
resource = Resource("shared")
def worker():
# Каждый поток получит свою ссылку
r = resource
# Работаем
# GIL гарантирует безопасное уменьшение refcount
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
# Все потоки завершились, refcount обнулился
# __del__ вызвался ровно один раз
2. Уменьшение refcount требует GIL
Каждое присваивание и удаление ссылки требует захвата GIL:
def operation_on_object():
obj = create_expensive_object() # refcount = 1, захват GIL
# Работаем
use(obj) # Доступ безопасен потому что есть ссылка
# obj выходит из scope
# Python: уменьши refcount (захват GIL)
# Если refcount == 0: вызови __del__ (всё ещё под GIL)
3. Проблема: del вызывается под GIL
Это может привести к deadlock-ам:
import threading
lock = threading.Lock()
class ProblematicObject:
def __del__(self):
# ВЫ ПЫТАЕТЕСЬ ЗАХВАТИТЬ ДРУГОЙ LOCK
with lock: # <-- Потенциальный deadlock!
print("Cleanup")
obj = ProblematicObject()
def worker():
# Один поток держит lock
with lock:
# Другой поток пытается удалить obj
# __del__ пытается захватить lock
# -> DEADLOCK (в многопоточной ситуации)
pass
Поэтому в __del__ не рекомендуется:
- Захватывать locks
- Вызывать blocking операции
- Обращаться к глобальному состоянию
Механизм: Refcount + GIL + Деferred Cleanup
Как Python это делает
// Упрощённо из CPython
void Py_DECREF(PyObject *obj) {
// GIL ДО ЭТОГО ЗАХВАЧЕН
if (--obj->ob_refcnt == 0) {
// Refcount обнулился
_Py_Dealloc(obj); // Вызываем __del__ и освобождаем память
}
// GIL ВСЁ ЕЩЁ ЗАХВАЧЕН ДО КОНЦА __del__
}
Цепочка удаления объектов
Это может привести к интересным эффектам:
class Parent:
def __init__(self):
self.child = Child()
def __del__(self):
print(f"Удаляю Parent")
class Child:
def __del__(self):
print(f"Удаляю Child")
parent = Parent()
del parent
# Вывод:
# Удаляю Parent
# Удаляю Child
# ВСЁ под GIL! Один thread блокируется на время всей цепи
Проблемы в реальных проектах
1. Задержка удаления с циклическими ссылками
class Node:
def __init__(self, name):
self.name = name
self.ref = None
def __del__(self):
print(f"Deleting {self.name}")
# Циклическая ссылка
node1 = Node("A")
node2 = Node("B")
node1.ref = node2
node2.ref = node1 # Цикл!
del node1, node2
# НЕ будет вызвано __del__!
# Объекты остаются в памяти до garbage collector
# GC обнаруживает цикл и удаляет (без GIL на новых версиях Python)
2. GC vs Refcount
Python использует двухуровневую систему:
import gc
print(gc.get_threshold()) # (700, 10, 10)
# Когда кол-во новых объектов > 700, запускается GC
# GC работает отдельно и удаляет циклические ссылки
# GC тоже требует GIL!
# Вы можете отключить GC для critical sections:
gc.disable() # ОПАСНО!
try:
# Критичный по времени код
process_data()
finally:
gc.enable()
gc.collect() # Явно запустить сборку
Python 3.13+: GIL становится optional
# В новых версиях можно отключить GIL при сборке Python:
# ./configure --disable-gil
# Тогда __del__ вызывается БЕЗ GIL
# Это решает deadlock проблемы но требует:
# - Синхронизации refcount (atomics, locks)
# - Тщательного тестирования
Практические рекомендации
1. Избегайте del, используйте context managers
# ❌ Опасно
class Resource:
def __del__(self):
self.file.close()
# ✅ Правильно
class Resource:
def __enter__(self):
self.file = open(...)
return self
def __exit__(self, *args):
self.file.close()
with Resource() as r:
# Гарантированно вызовется __exit__
process(r)
2. Не захватывайте locks в del
# ❌ Плохо
class Handler:
def __del__(self):
with self.lock: # DEADLOCK риск
cleanup()
# ✅ Хорошо
class Handler:
def cleanup(self):
with self.lock:
# явное управление
pass
def __del__(self):
# Простое удаление без locks
pass
3. Мониторьте утечки памяти
import tracemalloc
tracemalloc.start()
# Ваш код
current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.1f} MB")
print(f"Peak: {peak / 1024 / 1024:.1f} MB")
Итоговая схема
Удаление объекта:
├── GIL захватывается
├── Счётчик ссылок уменьшается
├── Если счётчик == 0:
│ ├── Вызывается __del__ (под GIL)
│ ├── Рекурсивно удаляются дочерние объекты (под GIL)
│ └── Объект удаляется из памяти
└── GIL отпускается
Циклические ссылки:
├── Не удаляются refcount (остаются в памяти)
├── Garbage Collector обнаруживает (периодически)
├── GC захватывает GIL
├── GC удаляет цикл
└── Потенциальная задержка (поток ждёт GIL)
Вот почему GIL так важен при уничтожении объектов — он гарантирует atomicity операций с памятью.