Когда использовать слабые ссылки в .Net?

56

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

Я также часто слышал, как разработчики AS3 говорили об использовании слабых ссылок для экономии занимаемой памяти, но, исходя из моих разговоров, кажется, что это добавляет сложности, не обязательно достигая намеченной цели, а поведение во время выполнения довольно непредсказуемо. Настолько, что многие просто отказываются от этого и вместо этого более осторожно управляют использованием памяти / оптимизируют свой код, чтобы он занимал меньше памяти (или делает компромисс между большим количеством циклов ЦП и меньшим объемом памяти).

Доктор Джон Харроп также указал в своем ответе, что слабые ссылки .Net не являются мягкими, и существует агрессивная коллекция слабых ссылок на gen0. Согласно MSDN , длинные слабые ссылки дают вам возможность воссоздать объект but the state of the object remains unpredictable.!

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

theburningmonk
источник
3
Вы уже обрисовали в общих чертах потенциальное использование этого. Конечно, есть и другие способы подойти к этим ситуациям, но существует несколько способов ободрать кошку. Если вы ищете пуленепробиваемое «вы всегда должны использовать WeakReference, когда X», я сомневаюсь, что вы найдете его.
2
@ itsme86 - я не ищу пуленепробиваемый вариант использования, просто те, для которых слабые ссылки подходят и имеют смысл. Например, случай использования кеша, потому что слабые ссылки собираются с таким нетерпением, что это приведет к большему количеству
4
Я немного разочарован тем, что многие получают голоса для закрытия. Я не прочь увидеть ответ или обсуждение этого вопроса (в b4 «Переполнение стека - это не форум»).
ta.speot.is
@theburningmonk Это смещение в обмен на увеличение памяти. В сегодняшних рамках сомнительно, что кто-либо может напрямую обратиться к инструменту WeakReference даже при реализации кеша, так как существуют всеобъемлющие системы кеширования, которые легко доступны.
Вот неловко излишне сложный пример их использования (для Шаблона слабых событий, который описан на ta.speot.is ниже)
Бенджол

Ответы:

39

Я нашел законное практическое применение слабых ссылок в следующих трех реальных сценариях, которые фактически произошли со мной лично:

Приложение 1: обработчики событий

Вы предприниматель. Ваша компания продает контрольные линии зажигания для WPF. Продажи отличные, но расходы на поддержку убивают вас. Слишком много клиентов жалуются на перегрузку процессора и утечки памяти при прокрутке экранов, заполненных линиями искр. Проблема в том, что их приложение создает новые линии искры, когда они появляются, но привязка данных не позволяет собирать старые. Чем ты занимаешься?

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

Приложение 2: изменяемые графы

Ты следующий Джон Кармак. Вы изобрели новое графическое представление поверхностей иерархического подразделения, которое делает игры Тима Суини похожими на Nintendo Wii. Очевидно, я не собираюсь рассказывать вам точно, как это работает, но все это сосредоточено на этом изменяемом графе, где соседи вершины могут быть найдены в Dictionary<Vertex, SortedSet<Vertex>>. Топология графа постоянно меняется, пока игрок бегает. Есть только одна проблема: ваша структура данных сбрасывает недостижимые подграфы во время работы, и вам нужно удалить их, иначе произойдет утечка памяти. К счастью, вы гений, так что вы знаете, что существует класс алгоритмов, специально предназначенных для поиска и сбора недоступных подграфов: сборщики мусора! Вы читаете отличную монографию Ричарда Джонса на эту темуно это оставляет вас озадаченным и обеспокоенным вашим предстоящим сроком. Чем ты занимаешься?

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

Приложение 3: Украшение деревьев

Вы свисаете с потолка циклической комнаты за клавиатурой. У вас есть 60 секунд, чтобы просмотреть БОЛЬШИЕ ДАННЫЕ, прежде чем вас найдут. Вы подготовили прекрасный потоковый парсер, который полагается на сборщик мусора для сбора фрагментов AST после их анализа. Но вы понимаете, что вам нужны дополнительные метаданные для каждого AST, Nodeи вам это нужно быстро. Чем ты занимаешься?

Вы можете использовать a Dictionary<Node, Metadata>для связи метаданных с каждым узлом, но, если вы не очистите их, сильные ссылки из словаря на старые узлы AST сохранят их живыми и утечку памяти. Решением является слабая хеш-таблица, в которой хранятся только слабые ссылки на ключи, а сборщик мусора собирает привязки ключ-значение, когда ключ становится недоступным. Затем, когда узлы AST становятся недоступными, они подвергаются сборке мусора, и их привязка значения ключа удаляется из словаря, оставляя соответствующие метаданные недоступными, поэтому они тоже собираются. Затем все, что вам нужно сделать после того, как ваша основная петля прервана, это проскользнуть обратно через вентиляционное отверстие, не забывая заменить его, как только войдет охранник.

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

Джон Харроп
источник
2
Слабые ссылки не будут работать для приложения 2, если недостижимые подграфы содержат циклы. Это потому, что слабая хеш-таблица обычно имеет слабые ссылки на ключи, но сильные ссылки на значения. Вам понадобится хеш-таблица, которая поддерживает строгие ссылки на значения только тогда, когда ключ еще доступен -> см. Эфемероны ( ConditionalWeakTableв .NET).
Даниэль
@ Даниель Разве GC не должен обрабатывать недоступные циклы? Как бы это не будет собранно , когда недостижимый цикл сильных ссылок будет собираться?
Бинки
О, я думаю, я вижу. Я просто предположил, что ConditionalWeakTableэто то, что приложения 2 и 3 будут использовать, в то время как некоторые люди в других постах на самом деле используют Dictionary<WeakReference, T>. Понятия не имею, почему - у вас всегда будет тонна нулевых WeakReferenceзначений со значениями, к которым ни один ключ не может получить доступ, независимо от того, как вы это делаете. Ridik.
Бинки
@binki: «Разве GC не способен обрабатывать недостижимые циклы? Как это не будет собираться, когда будет собираться недостижимый цикл сильных ссылок?». У вас есть словарь, связанный с уникальными объектами, которые невозможно воссоздать. Когда один из ваших ключевых объектов становится недоступным, он может быть собран мусором, но соответствующее значение в словаре даже не будет считаться теоретически недоступным, потому что обычный словарь будет содержать сильную ссылку на него, сохраняя его живым. Таким образом, вы используете слабый словарь.
Джон Харроп
@Daniel: "Слабые ссылки не будут работать для приложения 2, если недоступные подграфы содержат циклы. Это потому, что слабая хеш-таблица обычно имеет слабые ссылки на ключи, но сильные ссылки на значения. Вам нужна хеш-таблица, которая поддерживает сильные ссылки на значения только тогда, когда ключ еще доступен ". Да. Вам, вероятно, лучше кодировать граф непосредственно с ребрами в качестве указателей, поэтому GC будет собирать его сам.
Джон Харроп
19

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

Документ Microsoft Слабые шаблоны событий .

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

...

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

ta.speot.is
источник
Этот URL автоматически выбирает самую последнюю версию .NET (в настоящее время 4.5), в которой «эта тема больше не доступна». Вместо этого работает выбор .NET 4.0 ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
maxp
13

Позвольте мне сначала высказать это и вернуться к этому:

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

Итак, начнем с самого начала:

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

Поэтому, когда у вас есть объект X- давайте определим его как экземпляр class Foo- он НЕ МОЖЕТ жить самостоятельно (в основном это правда); Точно так же, как «Ни один человек не является островом», есть только несколько способов, которыми объект может быть переведен в Islandhood - хотя это называется быть корнем GC в CLR. Быть корнем GC или иметь установленную цепочку соединений / ссылок на корень GC - это, в основном, то, что определяет, Foo x = new Foo()собирается ли мусор или нет .

Если вы не можете вернуться к какому-либо корню GC с помощью кучи или стека, вы фактически осиротели и, вероятно, будете отмечены / собраны в следующем цикле.

На данный момент, давайте посмотрим на некоторые ужасно надуманные примеры:

Во-первых, наши Foo:

public class Foo 
{
    private static volatile int _ref = 0;
    public event EventHandler FooEvent;
    public Foo()
    {
        _ref++;
        Console.WriteLine("I am #{0}", _ref);
    }
    ~Foo()
    {
        Console.WriteLine("#{0} dying!", _ref--);
    }
}

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

Теперь давайте посмотрим на FooConsumer:

public class NastySingleton
{
    // Static member status is one way to "get promoted" to a GC root...
    private static NastySingleton _instance = new NastySingleton();
    public static NastySingleton Instance { get { return _instance;} }

    // testing out "Hard references"
    private Dictionary<Foo, int> _counter = new Dictionary<Foo,int>();
    // testing out "Weak references"
    private Dictionary<WeakReference, int> _weakCounter = new Dictionary<WeakReference,int>();

    // Creates a strong link to Foo instance
    public void ListenToThisFoo(Foo foo)
    {
        _counter[foo] = 0;
        foo.FooEvent += (o, e) => _counter[foo]++;
    }

    // Creates a weak link to Foo instance
    public void ListenToThisFooWeakly(Foo foo)
    {
        WeakReference fooRef = new WeakReference(foo);
        _weakCounter[fooRef] = 0;
        foo.FooEvent += (o, e) => _weakCounter[fooRef]++;
    }

    private void HandleEvent(object sender, EventArgs args, Foo originalfoo)
    {
        Console.WriteLine("Derp");
    }
}

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

// Our foo
var f = new Foo();

// Create a "hard reference"
NastySingleton.Instance.ListenToThisFoo(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Теперь, исходя из вышесказанного, ожидаете ли вы, что объект, на который когда-то ссылались, fбудет «коллекционируемым»?

Нет, потому что есть другой объект, который теперь содержит ссылку на него - Dictionaryв этом Singletonстатическом экземпляре.

Хорошо, давайте попробуем слабый подход:

f = new Foo();
NastySingleton.Instance.ListenToThisFooWeakly(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
// This should collect # 2 - you'll see a "#2 dying"
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Теперь, когда мы ударили нашу ссылку на «это Fooбыло когда- fто», больше нет «жестких» ссылок на объект, так что его можно собрать - WeakReferenceсозданный слабым слушателем не помешает этому.

Хорошие варианты использования:

  • Обработчики событий (хотя сначала прочтите это: Слабые события в C # )

  • У вас есть ситуация, когда вы вызываете «рекурсивную ссылку» (т. Е. Объект A ссылается на объект B, который ссылается на объект A, также называемый « утечкой памяти») (edit: derp, конечно, это не не правда)

  • Вы хотите «вещать» что-то на коллекцию объектов, но не хотите, чтобы они поддерживали их жизнь; List<WeakReference>может поддерживаться легко, и даже обрезают, удаляя , гдеref.Target == null

JerKimball
источник
1
Что касается вашего второго варианта использования, сборщик мусора прекрасно обрабатывает циклические ссылки. «объект A относится к объекту B, который относится к объекту A» определенно не является утечкой памяти.
Джо Дейли
@JoeDaley Я согласен. .NET GC использует алгоритм отметки и очистки, который (я полагаю, я правильно помню это) помечает все объекты для сбора, а затем следует ссылки из «корней» (ссылки на объекты в стеке, статические объекты), снимая пометки с объектов для сбора , Если круговая ссылка существует, но ни один из объектов не доступен из корня, объекты не помечаются для сбора и, следовательно, могут быть собраны.
ta.speot.is
1
@JoeDaley - Вы оба, конечно, правы - торопили это там к концу ... Я исправлю это.
JerKimball
4

введите описание изображения здесь

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

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

  1. указатели
  2. Сильные Ссылки
  3. Слабые ссылки

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


источник
1

Очень наглядным примером слабых ссылок, используемых для хорошего эффекта, является ConditionalWeakTable , который используется DLR (среди других мест) для присоединения дополнительных «элементов» к объектам.

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

Но мне кажется, что все случаи использования слабых ссылок появились после того, как они были добавлены в язык, так как слабые ссылки были частью .NET начиная с версии 1.1. Это просто то, что вы хотели бы добавить, так что отсутствие детерминированного разрушения не загонит вас в угол, если говорить о языковых особенностях.

GregRos
источник
Я действительно обнаружил, что, хотя в таблице используется концепция слабых ссылок, фактическая реализация не включает WeakReferenceтип, так как ситуация намного сложнее. Он использует различные функциональные возможности, предоставляемые CLR.
GregRos
-2

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

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

Кроме того, если у вас есть большой объект в вашем приложении (какая-то большая справочная таблица и т. Д.), Его следует использовать довольно редко, и воссоздание такого объекта не является очень дорогой процедурой. Тогда лучше иметь недельный справочник, чтобы освободить память, когда она вам действительно нужна.

Ph0en1x
источник
5
но проблема со слабыми ссылками (см. ответ, на который я ссылался) состоит в том, что они очень охотно собираются, и коллекция не связана с доступностью пространства памяти. Таким образом, вы получаете больше кеша, когда нет никакой нагрузки на память.
1
Но для вашего второго замечания о больших объектах, документ MSDN утверждает, что, хотя длинные слабые ссылки позволяют воссоздать объект, его состояние остается непредсказуемым. Если вы собираетесь воссоздавать его с нуля каждый раз, зачем использовать слабую ссылку, когда вы можете просто вызвать функцию / метод, чтобы создать ее по требованию и вернуть временный экземпляр?
Есть одна ситуация, в которой полезно кэширование: если вы будете часто создавать неизменяемые объекты, многие из которых окажутся идентичными (например, чтение многих строк из файла, который, как ожидается, будет иметь много дубликатов), каждая строка будет создана как новый объект , но если строка соответствует другой строке, на которую уже существует ссылка, эффективность использования памяти может быть улучшена, если новый экземпляр будет оставлен и ссылка на ранее существующий экземпляр будет заменена. Обратите внимание, что эта замена полезна, потому что другая ссылка в любом случае сохраняется. Если это не код, то следует сохранить новый.
суперкат