Шаблон подсчета ссылок для языков с управлением памятью?


11

Java и .NET имеют замечательные сборщики мусора, которые управляют памятью, и удобные шаблоны для быстрого освобождения внешних объектов ( Closeable, IDisposable), но только если они принадлежат одному объекту. В некоторых системах ресурс может потребляться независимо двумя компонентами и освобождаться только тогда, когда оба компонента освобождают ресурс.

В современном C ++ вы могли бы решить эту проблему с помощью a shared_ptr, который определенно освободил бы ресурс, когда все shared_ptrбыли уничтожены.

Существуют ли какие-либо документированные, проверенные шаблоны для управления и освобождения дорогих ресурсов, у которых нет единственного владельца в объектно-ориентированных, недетерминированных системах сбора мусора?


1
Вы видели автоматический подсчет ссылок Clang , также используемый в Swift ?
ОАО

1
@JoshCaswell Да, и это решило бы проблему, но я работаю в мусорном пространстве.
К. Росс

8
Подсчет ссылок - это стратегия сбора мусора.
Йорг Миттаг

Ответы:


15

В общем, вы избегаете этого, имея одного владельца - даже на неуправляемых языках.

Но принцип тот же для управляемых языков. Вместо того, чтобы немедленно закрывать дорогой ресурс, Close()вы уменьшаете счетчик (увеличивается на Open()/ Connect()/ и т. Д.), Пока не достигнете 0, и в этот момент закрытие фактически закрывает. Скорее всего, он будет выглядеть и вести себя как шаблон Flyweight.


Об этом я тоже думал, но есть ли задокументированный шаблон для этого? Flyweight, конечно, похож, но специально для памяти, как его обычно определяют.
К. Росс

@ C.Ross Похоже, это тот случай, когда финализаторы поощряются. Вы можете использовать класс-оболочку для неуправляемого ресурса, добавив в этот класс финализатор для освобождения ресурса. Вы также можете реализовать его IDisposable, вести подсчет для высвобождения ресурса как можно скорее и т. Д. Вероятно, самое лучшее во многих случаях - иметь все три, но финализатор, вероятно, является наиболее важной частью, а IDisposableреализация - наименее критичный.
Panzercrisis

11
@Panzercrisis за исключением того, что финализаторы не гарантированно работают, и особенно не гарантируется, что они будут работать быстро .
Caleth

@Caleth Я думал, что подсчет поможет с быстротой. Насколько они вообще не работают, вы имеете в виду, что CLR может просто не обойтись до конца программы, или вы имеете в виду, что они могут быть дисквалифицированы сразу?
Panzercrisis


14

На языке сборки мусора (где GC не является детерминированным) невозможно надежно связать очистку ресурса, кроме памяти, с временем жизни объекта: невозможно определить, когда объект будет удален. Конец срока службы полностью на усмотрение сборщика мусора. GC только гарантирует, что объект будет жить, пока он достижим. Как только объект становится недоступным, он может быть очищен в какой-то момент в будущем, что может включать запуск финализаторов.

Понятие «владение ресурсами» в действительности не применимо к языку GC. Система GC владеет всеми объектами.

То, что предлагают эти языки с помощью try-with-resource + Closeable (Java), с использованием операторов + IDisposable (C #) или с помощью операторов + менеджеров контекста (Python), позволяет потоку управления (! = Объекты) содержать ресурс, который закрывается, когда поток управления покидает область действия. Во всех этих случаях это похоже на автоматически вставляемый try { ... } finally { resource.close(); }. Время жизни объекта, представляющего ресурс, не связано с временем жизни ресурса: объект может продолжать жить после того, как ресурс был закрыт, и объект может стать недоступным, пока ресурс еще открыт.

В случае локальных переменных эти подходы эквивалентны RAII, но их необходимо явно использовать на сайте вызовов (в отличие от деструкторов C ++, которые будут запускаться по умолчанию). Хорошая IDE будет предупреждать, когда это опущено.

Это не работает для объектов, на которые ссылаются из местоположений, отличных от локальных переменных. Здесь не имеет значения, есть ли одна или несколько ссылок. Ссылку на ресурс через ссылки на объекты можно перевести на владение ресурсом через поток управления, создав отдельный поток, содержащий этот ресурс, но потоки также являются ресурсами, которые необходимо отбрасывать вручную.

В некоторых случаях можно делегировать владение ресурсами вызывающей функции. Вместо временных объектов, ссылающихся на ресурсы, которые они должны (но не могут) надежно очистить, вызывающая функция содержит набор ресурсов, которые необходимо очистить. Это работает только до тех пор, пока время жизни любого из этих объектов не истечет время жизни функции, и, следовательно, ссылается на ресурс, который уже был закрыт. Это не может быть обнаружено компилятором, если язык не имеет отслеживания Rust-подобного владения (в этом случае уже есть лучшие решения для этой проблемы управления ресурсами).

Это оставляет единственное жизнеспособное решение: ручное управление ресурсами, возможно, путем подсчета ссылок самостоятельно. Это подвержено ошибкам, но не невозможно. В частности, необходимость думать о владении необычна для языков GC, поэтому существующий код может не быть достаточно явным о гарантиях владения.


3

Много хорошей информации из других ответов.

Тем не менее, чтобы быть явным, шаблон, который вы можете искать, заключается в том, что вы используете небольшие объекты, принадлежащие одному, для RAII-подобной конструкции потока управления через usingи IDisposeв сочетании с (более крупным, возможно, подсчитанным числом ссылок) объектом, который содержит некоторые (работающие система) ресурсов.

Таким образом, есть небольшие неразделенные объекты с одним владельцем, которые (через меньший объект IDisposeи usingконструкцию потока управления) могут, в свою очередь, информировать более крупный общий объект (возможно, custom Acquire& Releaseметоды).

(Эти Acquireи Releaseспособы , приведенные ниже, а затем также доступны за пределами с использованием конструкции, но без безопасности tryподразумевается в using.)


Пример в C #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}

Если это должен быть C # (как это выглядит), то ваша реализация Reference <T> немного неверна. В контракте IDisposable.Disposeговорится, что вызов Disposeодного и того же объекта несколько раз должен быть запрещен. Если бы я реализовал такой шаблон, я бы также сделал Releaseприватным, чтобы избежать ненужных ошибок и использовал делегирование вместо наследования (удалите интерфейс, предоставьте простой SharedDisposableкласс, который можно использовать с произвольными одноразовыми объектами), но это больше вопрос вкуса.
Voo

@ Хорошо, хорошо, спасибо!
Эрик Эйдт

1

Подавляющее большинство объектов в системе обычно должно соответствовать одному из трех шаблонов:

  1. Объекты, состояние которых никогда не изменится, и ссылки на которые хранятся исключительно как средство инкапсуляции состояния. Объекты, которые содержат ссылки, не знают и не заботятся о том, содержат ли другие объекты ссылки на тот же объект.

  2. Объекты, которые находятся под исключительным контролем единственной сущности, которая является единственным владельцем всех состояний в ней, и используют объект исключительно как средство инкапсуляции в нем (возможно, изменяемого) состояния.

  3. Объекты, которые принадлежат одному объекту, но которые другим объектам разрешено использовать ограниченным образом. Владелец объекта может использовать его не только как средство инкапсуляции состояния, но также инкапсулировать отношения с другими объектами, которые его разделяют.

Отслеживание сборки мусора работает лучше, чем подсчет ссылок для # 1, потому что коду, использующему такие объекты, не нужно делать ничего особенного, когда это делается с последней оставшейся ссылкой. Подсчет ссылок не нужен для # 2, потому что у объектов будет ровно один владелец, и он будет знать, когда ему больше не нужен объект. Сценарий № 3 может представлять некоторые трудности, если владелец объекта убивает его, в то время как другие объекты все еще содержат ссылки; даже в этом случае отслеживание GC может быть лучше, чем подсчет ссылок, при условии, что ссылки на мертвые объекты будут надежно идентифицироваться как ссылки на мертвые объекты, пока существуют такие ссылки.

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


0

Общая собственность редко имеет смысл

Этот ответ может быть немного несоответствующим, но я должен спросить, сколько случаев имеет смысл с точки зрения пользователя разделять владение ? По крайней мере, в доменах, в которых я работал, их практически не было, потому что в противном случае это означало бы, что пользователю не нужно просто удалять что-то один раз из одного места, а явно удалять это от всех соответствующих владельцев, прежде чем ресурс действительно будет удален из системы.

Часто это инженерная идея более низкого уровня, которая предотвращает уничтожение ресурсов, пока к ним все еще кто-то обращается, например, другой поток. Часто, когда пользователь просит закрыть / удалить / удалить что-то из программного обеспечения, его следует удалить как можно скорее (когда это безопасно удалить), и это, безусловно, не должно задерживаться и вызывать утечку ресурсов до тех пор, пока приложение работает.

Например, игровой актив в видеоигре может ссылаться на материал из библиотеки материалов. Мы, конечно, не хотим, скажем, сбоя висящего указателя, если материал удален из библиотеки материалов в одном потоке, в то время как другой поток все еще получает доступ к материалу, на который ссылается игровой актив. Но это не значит, что игровым активам не имеет смысла делиться правами собственности на материалы, на которые они ссылаются, с библиотекой материалов. Мы не хотим заставлять пользователя явно удалять материал из библиотеки ресурсов и материалов. Мы просто хотим убедиться, что материалы не будут удалены из библиотеки материалов, единственного разумного владельца материалов, до тех пор, пока другие потоки не закончат доступ к материалу.

Утечки ресурсов

Тем не менее, я работал с бывшей командой, которая использовала GC для всех компонентов программного обеспечения. И хотя это действительно помогло убедиться, что у нас никогда не было уничтоженных ресурсов, в то время как другие потоки все еще обращались к ним, мы вместо этого получили свою долю утечек ресурсов .

И это были не тривиальные утечки ресурсов, которые расстраивают только разработчиков, например, утечка памяти в килобайтах после часового сеанса. Это были эпические утечки, часто гигабайты памяти во время активного сеанса, что приводило к сообщениям об ошибках. Потому что теперь, когда ссылка на владение ресурсом (и, следовательно, на него делятся владениями), скажем, между 8 различными частями системы, требуется только один, чтобы не удалить ресурс в ответ на запрос пользователя об удалении его для него. быть утечкой и, возможно, на неопределенный срок.

Так что я никогда не был большим поклонником GC или подсчета ссылок, применяемых в широком масштабе, потому что им было легко создавать утечку программного обеспечения. Раньше было бы зависание свисающего указателя, которое легко обнаружить, превращается в очень трудно обнаруживаемую утечку ресурса, которая может легко попасть под радар тестирования.

Слабые ссылки могут смягчить эту проблему, если язык / библиотека предоставляют их, но я обнаружил, что команде разработчиков смешанных наборов навыков трудно постоянно использовать слабые ссылки в случае необходимости. И эта проблема была связана не только с внутренней командой, но и с каждым разработчиком плагинов для нашего программного обеспечения. Они также могут легко вызвать утечку ресурсов в системе, просто сохраняя постоянную ссылку на объект таким образом, что затрудняется отследить плагин как виновника, поэтому мы также получили львиную долю сообщений об ошибках, полученных из наших программных ресурсов. утечка просто потому, что плагин, чей исходный код был вне нашего контроля, не смог выпустить ссылки на эти дорогие ресурсы.

Решение: отложено, периодическое удаление

Поэтому позднее мое решение, которое я применил к своим личным проектам, которые дали мне лучшее из того, что я нашел в обоих мирах, заключалось в устранении концепции, которая, referencing=ownershipтем не менее, отложила уничтожение ресурсов.

В результате, теперь, когда пользователь делает что-то, что приводит к необходимости удаления ресурса, API выражается в терминах простого удаления ресурса:

ecs->remove(component);

... которая моделирует пользовательскую логику очень простым способом. Тем не менее, ресурс (компонент) не может быть удален сразу же, если есть другие системные потоки в их фазе обработки, где они могут одновременно обращаться к одному и тому же компоненту.

Таким образом, эти потоки обработки тут и там дают время, которое позволяет потоку, похожему на сборщик мусора, просыпаться и « останавливать мир » и уничтожать все ресурсы, которые были запрошены для удаления, при этом блокируя потоки от обработки этих компонентов, пока он не завершится. , Я настроил это так, чтобы объем работы, который необходимо выполнить здесь, был, как правило, минимальным и не оказывал заметного влияния на частоту кадров.

Сейчас я не могу сказать, что это какой-то проверенный и хорошо документированный метод, но я уже несколько лет использую его без головной боли и утечек ресурсов. Я рекомендую исследовать подходы, подобные этому, когда ваша архитектура может соответствовать такой модели параллелизма, поскольку она намного менее сложна, чем сборщик мусора или повторный подсчет, и не рискует утечек этих типов ресурсов, попадающих под радар тестирования.

Единственное место, где я нашел повторный подсчет или GC, полезно для постоянных структур данных. В этом случае это территория структуры данных, далеко оторванная от интересов конечного пользователя, и там действительно имеет смысл для каждой неизменной копии иметь потенциальное совместное владение одними и теми же неизмененными данными.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.