← Назад к вопросам

Как задействован 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 операций с памятью.