Слабые ссылки - насколько они полезны?

в последнее время я обдумывал некоторые идеи автоматического управления памятью - в частности, я смотрел на реализацию менеджера памяти на основе подсчета ссылок. Конечно, все знают, что циркулярные ссылки убивают наивный подсчет ссылок. Решение: слабые ссылки. Лично я ненавижу, используя слабые ссылки таким образом (есть и другие, более интуитивные способы борьбы с этим, через обнаружение цикла), но это заставило меня задуматься: где еще может быть слабая ссылка полезно?

Я полагаю, что должна быть какая-то причина, по которой они существуют, особенно в языках с трассировкой сборки мусора, которые не страдают от циклической ссылочной ловушки (C# и Java-это те, с которыми я знаком, и Java даже имеет три вида слабых ссылок!). Когда я пытался найти для них надежные варианты использования, я в значительной степени просто получил идеи, такие как "использовать их для реализации кэшей" (я видел это несколько раз на SO). Мне это тоже не нравится, так как они полагаются на тот факт, что трассировочный GC, скорее всего, не будет собирать объект сразу после того, как на него больше нет строгих ссылок, за исключением ситуаций с низкой памятью. Такие случаи абсолютно недопустимы при подсчете ссылок GC, так как объект уничтожается тут после того, как на него больше не ссылается (за исключением, возможно, в случае циклов).

но это действительно заставляет меня задуматься: как может быть полезна слабая ссылка? Если вы не можете рассчитывать на то, что он ссылается на объект, и его не нужно для таких вещей, как нарушение циклов, тогда зачем использовать один?

4 ответов


обработчики событий-это хороший вариант использования для слабых ссылок. Объект, который запускает события, нуждается в ссылке на объекты для вызова обработчиков событий, но вы обычно не хотите, чтобы удержание ссылки производителя событий препятствовало gc'D потребителей событий. Скорее всего, вы хотите, чтобы у производителя событий была слабая ссылка, и он будет отвечать за проверку наличия ссылочного объекта.


но что действительно заставляет меня задуматься: как может слабая ссылка, возможно быть полезным? Если вы не можете рассчитывать на то, что он ссылается на объект, а его нет нужно для таких вещей, как разрыв циклов, тогда зачем использовать один?

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

class Foo
{
    ...
    // Stores a weak reference to bar. 'Foo' does not
    // own bar.
    private Bar bar;

    // Stores a strong reference to 'Baz'. 'Foo' does
    // own Baz.
    private strong Baz baz;
}

... между тем обратное для местных жителей внутри функции/метода:

void some_function()
{
    // Stores a strong reference to 'Bar'. It will
    // not be destroyed until it goes out of scope.
    Bar bar = ...;

    // Stores a weak reference to 'Baz'. It can be
    // destroyed before the weak reference goes out 
    // of scope.
    weak Baz baz_weak = ...;

    ...

    // Acquire a strong reference to 'Baz'.
    Baz baz = baz_weak;
    if (baz)
    {
        // If 'baz' has not been destroyed,
        // do something with it.
        baz.do_something();
    }       
}

История Ужасов

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

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

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

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

Логические Утечки

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

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

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

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

страшно!

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

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

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

утечка GC

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

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

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

GC не является практической защитой от утечек памяти, совсем наоборот. Если бы это было так, наименее протекающие приложения в мире были бы написаны на языках, поддерживающих GC, таких как Flash, Java, JavaScript, C#, а самое протекающее программное обеспечение было бы написано на языках с самым ручным управлением памятью, таким как C, в этот момент ядро Linux должно быть чертовски протекающим система, которая потребует перезапуска каждые час или два, чтобы уменьшить использование памяти. Но это не так. Часто бывает наоборот, когда самые протекающие приложения пишутся против GC, и это потому, что GC на самом деле затрудняет предотвращение логических утечек. Где это помогает избежать физических утечек (но физические утечки достаточно легко обнаружить и избежать в первую очередь независимо от того, какой язык вы используете), и где это помогает, чтобы предотвратить обрыв указателя критически важное программное обеспечение, где более желательно утечка памяти, чем сбой, потому что на карту поставлена жизнь человека или потому, что сбой может привести к тому, что сервер будет недоступен в течение нескольких часов подряд. Я не работаю в критически важных областях; я работаю в областях производительности и памяти с Epic datasets обрабатываются с каждым кадром, который отображается.

в конце концов, все, что нам нужно сделать, чтобы создать логическую утечку с GC, это:

class Foo
{
     // This makes 'Foo' instances cause 'bar' to leak, preventing
     // it from being destroyed until the 'Foo' instances are also
     // destroyed unless the 'Foo' instances set this to a null 
     // reference at the right time (ex: when the user requests 
     // to remove whatever Bar is from the software).
     private Bar bar;
}

... но слабый ссылки не рискуют этой проблемой. Когда вы смотрите на миллионы LOC, как указано выше, с одной стороны, и epic memory leaks с другой, это довольно кошмарный сценарий, когда вам нужно исследовать, какой аналогический Foo не удалось установить аналогическое Bar к нулевой ссылке в соответствующее время, потому что это та часть, которая так страшно: код работает просто отлично, пока вы игнорируете гигабайты утечки памяти. Ничто не вызывает каких-либо ошибок/исключений, ошибок утверждения, так далее. Ничего не разбивается. Весь блок и интеграция проходят без жалобы. Все работает за исключением того, что она протекает гигабайтами памяти вызывает жалобы пользователей налево и направо, пока вся команда ломают голову о том, что части кода неплотные и, что не а ОК пытается уладить конфликт путем прагматично предполагая, что пользователь сохраняет свою работу и перезапустите программу каждые полчаса, как будто это должно быть какое-то решение.

слабые ссылки очень помогают

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

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

используя мой пример графика сцены, список исключений камеры не должен владеть объектами сцены, уже принадлежащими графику сцены. Логически это не имеет смысла. Если мы находимся на чертежной доске, никто не должен думать: "да, камеры также должны собственные объекты сцены в дополнение к графической сцены себя."

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

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

полезность слабых ссылок в командной среде

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

безопасность

они могут показаться не столь полезными в том же смысле, что программист сборки может не найти много пользы для безопасности типов. В конце концов, он может делать все, что ему нужно, только с необработанными битами и байтами и соответствующими инструкциями по сборке. Тем не менее, type safety помогает легче обнаруживать человеческие ошибки, заставляя разработчиков более явно выражать то, что они хотят делать, и ограничивая то, что им разрешено делать с определенным типом. Я вижу слабые ссылки в подобном смысле. Они помогают обнаруживать человеческие ошибки, которые в противном случае привели бы к утечке ресурсов, если бы не использовались слабые ссылки. Это намеренно накладывает ограничения на себя, как,"хорошо, это слабая ссылка на объект, поэтому он не может продлить срок службы и вызвать логическую утечку" что неудобно, но так же безопасность типа для программиста сборки. Это может помочь предотвратить некоторые очень неприятные жуки.

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

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

typedef Strong<Mesh> MeshRef;
typedef Weak<Mesh> MeshWeakRef;

... это был проприетарный GC, реализованный на C++, работающий в отдельный поток. Я изменил его на это:

typedef Weak<Mesh> MeshRef;
typedef Strong<Mesh> MeshStrongRef;

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


объект, на который ссылается WeakReference, будет доступен до процесса gc.

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

кстати, SoftReference отличается от WeakReference, потому что Связанный объект будет собран только тогда, когда памяти недостаточно. Таким образом, SoftReference будет использоваться для создания глобального кэша обычно.


Я часто использую WeakReference в сочетании с ThreadLocal или InheritableThreadLocal. Если мы хотим, чтобы значение было доступно для нескольких потоков, пока оно имеет смысл, но затем удалите значение из этих потоков, мы не можем освободить память сами, потому что нет способа вмешаться в ThreadLocal значение в потоке, отличном от текущего. Однако то, что вы можете сделать, это поместить значение в WeakReference в этих других потоках (когда значение создается-это предполагает, что один и тот же экземпляр является общим обратите внимание, что это имеет смысл только тогда, когда только подмножество потоков должно иметь доступ к этому значению, или вы просто используете статику) и храните твердую ссылку в другом ThreadLocal для некоторого рабочего потока, который будет удалять значение. Затем, когда значение перестает быть значимым, вы можете попросить рабочий поток удалить жесткую ссылку, которая заставляет значения во всех других потоках немедленно запрашиваться для сборки мусора (хотя они могут не быть мусор-собирается немедленно, поэтому стоит иметь какой-то другой способ предотвратить доступ к значению).