В D, как известно, нет встроенного способа удалить объект – то есть, освободить занятую им память. Функция destroy лишь вызывает деструктор и помечает объект как недействительный, но фактически память высвобождается в следующем цикле сборки мусора. dlib, будучи библиотекой для разработки приложений реального времени, предоставляет альтернативные механизмы управления памятью с возможностью удалять объекты вручную – в моменты, явно определяемые программистом, а не логикой сборщика мусора. Это накладывает на программиста определенную степень ответственности, так как стопроцентно ручное управление памятью – занятие довольно хардкорное. Я написал на Medium статью на эту тему, где описал парадигму владения (ownership), рекомендуемую при работе с dlib. Суть ее в том, что удаление данных автоматически выполняет объект-владелец этих данных, когда кто-то – вы сами или его собственный владелец – удаляет его самого. Таким образом, вы у себя в коде расставляете единичные функции Delete только в ключевых местах, когда ваше приложение переходит из одного режима в другой, а вся рутинная работа по удалению данных ложится на иерархию объектов-владельцев. Например, если это игра, то вы можете удалить текущую сцену, когда пользователь завершает уровнень, проигрывает, выходит в главное меню или загружает сохранение. Если объект сцены является владельцем всех ее данных, то они будут автоматически удалены.

Но при этом может возникнуть неожиданная проблема. Допустим, у вас есть некий глобальный менеджер игры, который каждый раз передает управление загруженной сцене. Сцена формирует нужные ей структуры данных, обрабатывает входящие события, реагирует на пользовательский ввод, обновляет изменяемое состояние и рендерит графику – то есть, совершает довольно много задач в цикле, полагаясь на то, что все ее данные находятся в памяти. Если вам нужно завершить работу сцены, то это равносильно удалению объектом самого себя – то есть, сцена обращается к корневому менеджеру с запросом о переключении в другой режим, и он ее удаляет. Можно ли в dlib так делать?

На первый взгляд, можно:

import dlib.core.memory;
import dlib.core.ownership;

class Host: Owner
{
    this()
    {
        super(null);
    }
    
    void kill(Owned obj)
    {
        deleteOwnedObject(obj);
    }
}

class Property: Owned
{
    Host host;
    
    this(Host host)
    {
        this.host = host;
        host.addOwnedObject(this);
    }
    
    void kill()
    {
        host.kill(this);
    }
}

void main()
{
    Host host = New!Host;
    Property prop = New!Property(host);
    prop.kill();
    Delete(host);
}

Но это работает только в простейшем случае, если вы не обращаетесь к данным экземпляра Property после удаления. Это звучит странно – кому придет в голову что-то делать с удаленным объектом? Но не все так просто. Рассмотрим такую модификацию нашего примера:

class Property: Owned
{
    Host host;
    string[5] data;
    
    this(Host host)
    {
        host.addOwnedObject(this);
        this.host = host;
        data = ["one", "two", "three", "four", "five"];
    }
    
    void doSomethingComplex()
    {
        foreach(i, v; data)
        {
            if (i == 3)
                host.kill(this);
            else
                writeln(v);
        }
    }
}

void main()
{
    Host host = New!Host;
    Property prop = New!Property(host);
    prop.doSomethingComplex();
    Delete(host);
}

Цикл в методе doSomethingComplex продолжится даже после удаления объекта – получается “зомби-метод”, поведение которого непредсказуемо. У меня, например, массив data просто исчез, и writeln печатает пустую строку.

Очевидно, что корректно было бы прервать цикл сразу после удаления. Но что, если удаление происходит где-то в недрах Host’а, и информация об этом к вам в цикл не возвращается? У меня в движке, например, многие классы слушают внешние события, и удаление сцены может произойти в виртуальном методе-обработчике события. При этом абстрактный класс слушателя событий, естественно, не учитывает возможность подобного самоубийства и не проверяет собственное существование. Сделать подобную проверку вообще не так-то просто, и выполнять ее перед любым телодвижением довольно накладно. Получается, что паттерн “самоубийцы” в его самой очевидной реализации плохо совместим с принципами ООП.

Что же делать? Выручает асинхронность! Вместо того, чтобы удалять объект сразу, его лучше поместить в некий “карантин” и удалить позже, когда он завершит свою работу. Например, хост может удалить его на следующей итерации игрового цикла. В нашем примере это будет выглядеть так:

import dlib.container.array;

class Host: Owner
{
    Array!Owned objectsToKill;
    
    this()
    {
        super(null);
    }
    
    void killAsync(Owned obj)
    {
        objectsToKill.append(obj);
    }
    
    void killObjects()
    {
        foreach(obj; objectsToKill)
            deleteOwnedObject(obj);
        objectsToKill.free();
    }
}

class Property: Owned
{
    Host host;
    string[5] data;
    
    this(Host host)
    {
        host.addOwnedObject(this);
        this.host = host;
        data = ["one", "two", "three", "four", "five"];
    }
    
    void doSomethingComplex()
    {
        foreach(i, v; data)
        {
            if (i == 3)
                host.killAsync(this);
            else
                writeln(v);
        }
    }
}

void main()
{
    Host host = New!Host;
    Property prop = New!Property(host);
    prop.doSomethingComplex();
    host.killObjects();
    Delete(host);
}

Получается простейшее подобие сборщика мусора, поведение которого вам полностью подконтрольно.

Written by Gecko

Разработчик компьютерной графики

Оставить комментарий

Ваш адрес email не будет опубликован.