Kiedy używać słabych referencji w .Net?

56

Osobiście nie spotkałem się z sytuacją, w której musiałem używać typu WeakReference w .Net, ale wydaje się, że popularne jest to, że powinno się go używać w pamięci podręcznej. Dr Jon Harrop w swojej odpowiedzi na to pytanie bardzo dobrze uzasadnił użycie WeakReferences w pamięci podręcznej .

Często słyszałem też, jak programiści AS3 mówią o używaniu słabych referencji, aby zaoszczędzić na pamięci, ale na podstawie przeprowadzonych rozmów wydaje się, że zwiększają złożoność, niekoniecznie osiągając zamierzony cel, a zachowanie w czasie wykonywania jest raczej nieprzewidywalne. Do tego stopnia, że ​​wielu po prostu się poddaje i zamiast tego ostrożniej zarządza zużyciem pamięci / optymalizuje kod, aby mniej obciążał pamięć (lub zmniejszał liczbę cykli procesora i zajmował mniej miejsca).

Dr Jon Harrop zwrócił także uwagę w swojej odpowiedzi, że słabe referencje .Net nie są miękkie, a na gen0 istnieje agresywny zbiór słabych referencji. Według MSDN długie słabe referencje dają możliwość odtworzenia obiektu but the state of the object remains unpredictable.!

Biorąc pod uwagę te cechy, nie mogę wymyślić sytuacji, w której przydatne byłyby słabe referencje, może ktoś mógłby mnie oświecić?

theburningmonk
źródło
3
Przedstawiłeś już potencjalne zastosowania. Oczywiście istnieją inne sposoby podejścia do takich sytuacji, ale istnieje więcej niż jeden sposób na skórowanie kota. Jeśli szukasz kuloodpornego „zawsze powinieneś używać WeakReference, gdy X”, wątpię, czy go znajdziesz.
2
@ itsme86 - Nie szukam kuloodpornego zastosowania, tylko takie, do których słabe referencje są odpowiednie i mają sens. Na przykład przypadek użycia pamięci podręcznej, ponieważ słabe referencje są gromadzone tak chętnie, że spowoduje to więcej
4
Jestem trochę rozczarowany, że zyskuje wiele głosów. Nie miałbym nic przeciwko zobaczeniu odpowiedzi lub dyskusji na ten temat (w b4 „Przepełnienie stosu nie jest forum”).
ta.speot.is
@theburningmonk To jest przesunięcie w zamian za zwiększenie pamięci. W dzisiejszych ramach wątpliwe jest, aby ktokolwiek sięgałby prosto do narzędzia WeakReference, nawet podczas implementacji pamięci podręcznej, ponieważ dostępne są kompleksowe systemy buforowania.
Oto zawstydzająco nadmiernie skomplikowany przykład korzystania z nich (w przypadku słabego wzorca zdarzeń opisanego poniżej przez
ta.speot.is

Odpowiedzi:

39

Znalazłem uzasadnione praktyczne zastosowania słabych referencji w następujących trzech rzeczywistych sytuacjach, które faktycznie mi się przydarzyły:

Aplikacja 1: Procedury obsługi zdarzeń

Jesteś przedsiębiorcą Twoja firma sprzedaje kontrolę iskierników dla WPF. Sprzedaż jest świetna, ale zabijają cię koszty wsparcia. Zbyt wielu klientów narzeka na zawieszenie procesora i wycieki pamięci, gdy przewijają ekrany pełne iskier. Problem polega na tym, że ich aplikacja tworzy nowe linie iskrowe, gdy pojawiają się w widoku, ale wiązanie danych zapobiega gromadzeniu śmieci przez stare. Co robisz?

Wprowadź słabe odniesienie między powiązaniem danych a formantem, aby samo powiązanie danych nie zapobiegało gromadzeniu elementów bezużytecznych. Następnie dodaj do swojej kontrolera finalizator, który zrywa powiązanie danych po ich zebraniu.

Aplikacja 2: Zmienne wykresy

Jesteś następnym Johnem Carmackiem. Wynalazłeś genialną, opartą na grafie reprezentację hierarchicznych powierzchni podziału, dzięki której gry Tima Sweeneya wyglądają jak Nintendo Wii. Oczywiście nie powiem ci dokładnie, jak to działa, ale wszystko koncentruje się na tym zmiennym wykresie, na którym można znaleźć sąsiadów wierzchołka Dictionary<Vertex, SortedSet<Vertex>>. Topologia wykresu zmienia się wraz z bieganiem gracza. Jest tylko jeden problem: twoja struktura danych zrzuca nieosiągalne subgrafy podczas działania i musisz je usunąć, inaczej wyciek pamięci. Na szczęście jesteś geniuszem, więc wiesz, że istnieje klasa algorytmów specjalnie zaprojektowanych do lokalizowania i gromadzenia nieosiągalnych subgrafów: śmieciarki! Przeczytałeś doskonałą monografię Richarda Jonesa na ten tematale wprawia Cię w zakłopotanie i niepokój o zbliżający się termin. Co robisz?

Po prostu zamieniając swój Dictionaryna słaby stół mieszający, możesz nałożyć na siebie istniejący GC i automatycznie zbierać dla ciebie nieosiągalne podgrupy! Powrót do przeglądania reklam Ferrari.

Aplikacja 3: Dekorowanie drzew

Zwisasz z sufitu cyklicznego pokoju przy klawiaturze. Masz 60 sekund na przeszukanie DUŻYCH DANYCH, zanim ktoś Cię znajdzie. Przyszedłeś przygotowany z pięknym parserem opartym na strumieniu, który polega na GC do zbierania fragmentów AST po ich przeanalizowaniu. Ale zdajesz sobie sprawę, że potrzebujesz dodatkowych metadanych na każdym AST Nodei potrzebujesz go szybko. Co robisz?

Możesz użyć a Dictionary<Node, Metadata>do powiązania metadanych z każdym węzłem, ale jeśli go nie wyczyścisz, silne odwołania ze słownika do starych węzłów AST utrzymają je przy życiu i spowodują wyciek pamięci. Rozwiązaniem jest słaba tablica skrótów, która utrzymuje tylko słabe odwołania do kluczy, a śmieci zbierają powiązania klucz-wartość, gdy klucz staje się nieosiągalny. Następnie, gdy węzły AST stają się nieosiągalne, są one odśmiecane, a ich powiązanie klucz-wartość jest usuwane ze słownika, pozostawiając odpowiednie metadane nieosiągalne, więc również zostaje zebrane. Następnie wszystko, co musisz zrobić po zakończeniu głównej pętli, to wślizgnąć się z powrotem przez otwór wentylacyjny, pamiętając o wymianie, gdy tylko pojawi się ochroniarz.

Zauważ, że we wszystkich trzech rzeczywistych aplikacjach, które mi się przydarzyły, chciałem, aby GC zbierała tak agresywnie, jak to możliwe. Dlatego są to legalne aplikacje. Wszyscy inni się mylą.

Jon Harrop
źródło
2
Słabe referencje nie będą działać dla aplikacji 2, jeśli nieosiągalne podgrupy zawierają cykle. Wynika to z tego, że słaba tabela skrótów zwykle zawiera słabe odwołania do kluczy, ale silne odniesienia do wartości. Potrzebujesz tabeli skrótów, która utrzymuje silne odwołania do wartości tylko wtedy, gdy klucz jest nadal osiągalny -> zobacz efemerydy ( ConditionalWeakTablew .NET).
Daniel,
@Daniel Czy GC nie powinien być w stanie obsłużyć nieosiągalnych cykli? Jak miałoby to nie być zbierane, gdy nieosiągalny cykl silnymi odniesieniami by być zbierane?
binki
Och, myślę, że rozumiem. Po prostu założyłem, że ConditionalWeakTabletego właśnie użyją aplikacje 2 i 3, podczas gdy niektóre osoby na innych postach faktycznie używają Dictionary<WeakReference, T>. Nie mam pojęcia, dlaczego - zawsze otrzymujesz tonę zerową WeakReferencez wartościami, do których żaden klucz nie może uzyskać dostępu, niezależnie od tego, jak to zrobisz. Ridik.
binki
@binki: „Czy GC nie powinien być w stanie obsłużyć nieosiągalnych cykli? Jak to nie zostanie zebrane, gdy zostanie zebrany nieosiągalny cykl silnych referencji?”. Masz słownik oparty na unikalnych obiektach, których nie można odtworzyć. Gdy jeden z twoich kluczowych obiektów stanie się nieosiągalny, można go wyrzucić, ale odpowiadająca mu wartość w słowniku nie będzie nawet uważana za teoretycznie nieosiągalną, ponieważ zwykły słownik zawiera silne odniesienie do niego, utrzymując go przy życiu. Więc używasz słabego słownika.
Jon Harrop
@Daniel: „Słabe odwołania nie będą działać dla aplikacji 2, jeśli nieosiągalne podgrupy zawierają cykle. Jest tak, ponieważ słaba tabela skrótów zwykle zawiera słabe odwołania do kluczy, ale silne odniesienia do wartości. Potrzebujesz tabeli skrótów, która utrzymuje silne odniesienia do wartości tylko wtedy, gdy klucz jest nadal osiągalny ”. Tak. Prawdopodobnie lepiej jest zakodować wykres bezpośrednio za pomocą krawędzi jako wskaźników, więc GC sam go zbierze.
Jon Harrop
19

Biorąc pod uwagę te cechy, nie mogę wymyślić sytuacji, w której przydatne byłyby słabe referencje, może ktoś mógłby mnie oświecić?

Dokument Microsoft Słabe wzorce zdarzeń .

W aplikacjach możliwe jest, że procedury obsługi dołączone do źródeł zdarzeń nie zostaną zniszczone w koordynacji z obiektem detektora, który podłączył procedurę obsługi do źródła. Ta sytuacja może prowadzić do wycieków pamięci. Windows Presentation Foundation (WPF) wprowadza wzorzec projektowy, którego można użyć do rozwiązania tego problemu, zapewniając dedykowaną klasę menedżera dla poszczególnych zdarzeń i implementując interfejs dla detektorów dla tego zdarzenia. Ten wzorzec projektowy jest znany jako wzorzec słabych zdarzeń.

...

Wzorzec słabych zdarzeń ma na celu rozwiązanie tego problemu wycieku pamięci. Wzorzec słabego zdarzenia może być używany za każdym razem, gdy detektor musi zarejestrować się w celu uzyskania zdarzenia, ale detektor nie wie wyraźnie, kiedy się wyrejestrować. Wzorzec słabych zdarzeń może być również użyty, ilekroć czas życia obiektu źródła przekracza czas użytecznego obiektu nasłuchiwania. (W tym przypadku użyteczność jest określana przez Ciebie.) Wzorzec słabych zdarzeń pozwala słuchaczowi zarejestrować się i odebrać zdarzenie, nie wpływając w żaden sposób na charakterystykę czasu życia obiektu. W efekcie domniemane odwołanie ze źródła nie określa, czy detektor kwalifikuje się do odśmiecania. Odwołanie jest słabym odwołaniem, a zatem nazewnictwo wzorca słabych zdarzeń i powiązanych interfejsów API. Nasłuchiwanie może zostać wyrzucone na śmietnik lub w inny sposób zniszczone, a źródło może być kontynuowane bez zachowania niemożliwych do pobrania odniesień procedury obsługi do zniszczonego teraz obiektu.

ta.speot.is
źródło
Ten adres URL automatycznie wybiera najnowszą wersję .NET (obecnie 4.5), w której „ten temat nie jest już dostępny”. Wybranie .NET 4.0 zamiast tego działa ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
maxp
13

Pozwól, że postawię to na pierwszym miejscu i wrócę do tego:

WeakReference jest przydatny, gdy chcesz mieć oko na obiekt, ale NIE chcesz, aby twoje obserwacje zapobiegały gromadzeniu tego obiektu

Zacznijmy od początku:

- przepraszam z góry za niezamierzone przestępstwo, ale wrócę na chwilę do poziomu „Dicka i Jane”, ponieważ nigdy nie można powiedzieć publiczności.

Więc kiedy masz obiekt X- określmy go jako instancję class Foo- NIE MOŻE on żyć sam (głównie prawda); W ten sam sposób, w jaki „Żaden człowiek nie jest wyspą”, istnieje tylko kilka sposobów, w jakie obiekt może awansować do Islandhood - chociaż nazywa się to korzeniem GC w CLR. Bycie rootem GC lub posiadanie ustalonego łańcucha połączeń / odniesień do roota GC jest w zasadzie tym, co decyduje o tym, czy Foo x = new Foo()śmieci są zbierane, czy nie .

Jeśli nie możesz wrócić do korzenia GC ani przez stertę, ani przez stos, jesteś skutecznie osierocony i prawdopodobnie zostaniesz oznaczony / zebrany w następnym cyklu.

W tym miejscu spójrzmy na kilka okropnie wymyślonych przykładów:

Po pierwsze 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--);
    }
}

Dość proste - nie jest bezpieczne dla wątków, więc nie próbuj tego, ale zachowuje przybliżoną „liczbę referencji” aktywnych instancji i dekrecji po ich sfinalizowaniu.

Teraz spójrzmy na 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");
    }
}

Mamy więc obiekt, który jest już własnym katalogiem głównym GC (cóż ... konkretnie, zostanie zrootowany przez łańcuch bezpośrednio do domeny aplikacji, w której działa ta aplikacja, ale to inny temat), który ma dwie metody zaczepienia się na Fooinstancji - przetestujmy to:

// 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();

Czy w związku z powyższym spodziewałbyś się, że obiekt, o którym kiedyś była mowa, fbędzie „kolekcjonerski”?

Nie, ponieważ istnieje inny obiekt, który teraz zawiera odniesienie do niego - Dictionaryw tej Singletoninstancji statycznej.

Ok, spróbujmy słabego podejścia:

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();

Teraz, kiedy mamy walnięcie nasze odniesienie do dokonywanych poza rynkiem Foo-to-było-raz w miesiącu f, nie nie są bardziej „twarde” odniesienia do obiektu, więc jest to dla kolekcjonerów - w WeakReferencestworzony przez słabego słuchacza nie będzie temu zapobiec.

Dobre przypadki użycia:

  • Procedury obsługi zdarzeń (chociaż najpierw przeczytaj: Słabe zdarzenia w C # )

  • Masz sytuację, w której spowodowałbyś „odwołanie rekurencyjne” (tj. Obiekt A odnosi się do obiektu B, który odnosi się do obiektu A, zwanego również „przeciekiem pamięci”) (edytuj: derp, oczywiście, że to nie jest to prawda)

  • Chcesz „nadawać” coś do zbioru obiektów, ale nie chcesz być tym, co utrzymuje je przy życiu; a List<WeakReference>można łatwo utrzymać, a nawet przyciąć, usuwając gdzieref.Target == null

JerKimball
źródło
1
Jeśli chodzi o drugi przypadek użycia, śmieciarz dobrze radzi sobie z okrągłymi odnośnikami. „obiekt A odnosi się do obiektu B, który odnosi się do obiektu A” zdecydowanie nie jest przeciekiem pamięci.
Joe Daley
@JoeDaley Zgadzam się. .NET GC wykorzystuje algorytm znakowania i wyciągnięcia po ścieżce, który (myślę, że dobrze to pamiętam) zaznacza wszystkie obiekty do kolekcji, a następnie podąża za referencjami z „korzeni” (referencje obiektów na stosie, obiekty statyczne), odznaczając obiekty do kolekcji . Jeśli istnieje odwołanie cykliczne, ale żaden z obiektów nie jest dostępny z katalogu głównego, obiekty nie są odznaczone do kolekcji, a zatem kwalifikują się do kolekcji.
ta.speot.is
1
@JoeDaley - Oboje oczywiście macie rację - pędziłem tam pod koniec ... Wyedytuję to.
JerKimball
4

wprowadź opis zdjęcia tutaj

Jak logiczne wycieki, które są naprawdę trudne do wyśledzenia, podczas gdy użytkownicy po prostu zauważają, że uruchamianie oprogramowania przez długi czas ma tendencję do zajmowania coraz większej ilości pamięci i spowalniania aż do ponownego uruchomienia? Ja nie.

Zastanów się, co się stanie, jeśli użytkownik, który zażąda usunięcia zasobu aplikacji powyżej, Thing2nie poradzi sobie odpowiednio z takim zdarzeniem w ramach:

  1. Wskaźniki
  2. Mocne referencje
  3. Słabe referencje

... i pod którym jednym z takich błędów prawdopodobnie zostanie złapany podczas testowania, a który nie i nie poleciałby pod radarem jak błąd myśliwca ukrycia. Współwłasność jest najczęściej bezsensownym pomysłem.


źródło
1

Bardzo ilustrującym przykładem słabych odniesień zastosowanych z dobrym skutkiem jest ConditionalWeakTable , która jest używana przez DLR (między innymi) do dołączania dodatkowych „elementów” do obiektów.

Nie chcesz, aby stół utrzymywał obiekt przy życiu. Ta koncepcja po prostu nie mogłaby działać bez słabych referencji.

Wydaje mi się jednak, że wszystkie zastosowania słabych referencji pojawiły się długo po ich dodaniu do języka, ponieważ słabe referencje są częścią .NET od wersji 1.1. Wygląda na to, że chciałbyś coś dodać, aby brak deterministycznego zniszczenia nie cofnął cię w kąt, jeśli chodzi o funkcje językowe.

GregRos
źródło
Odkryłem, że chociaż tabela wykorzystuje koncepcję słabych referencji, rzeczywista implementacja nie obejmuje tego WeakReferencetypu, ponieważ sytuacja jest znacznie bardziej złożona. Wykorzystuje różne funkcje udostępniane przez CLR.
GregRos
-2

Jeśli masz warstwę pamięci podręcznej zaimplementowaną w języku C #, znacznie lepiej jest umieścić dane w pamięci podręcznej jako słabe odwołania, może to pomóc w poprawie wydajności warstwy pamięci podręcznej.

Pomyśl, że to podejście można również zastosować do implementacji sesji. Ponieważ sesja jest przez długi czas przedmiotem życia, może to być przypadek, gdy nie ma pamięci dla nowego użytkownika. W takim przypadku znacznie lepiej będzie usunąć jakiś obiekt sesji użytkownika, a następnie zgłosić wyjątek OutOfMemoryException.

Ponadto, jeśli masz duży obiekt w aplikacji (jakaś duża tabela odnośników itp.), Należy go używać dość rzadko, a odtworzenie takiego obiektu nie jest bardzo kosztowną procedurą. Więc lepiej, aby to było jak tydzień odniesienia, aby mieć sposób na uwolnienie pamięci, gdy naprawdę tego potrzebujesz.

Ph0en1x
źródło
5
problem ze słabymi referencjami (patrz odpowiedź, do której się powołałem) polega na tym, że są one bardzo chętnie gromadzone, a kolekcja nie jest powiązana z dostępnością miejsca w pamięci. W rezultacie brakuje więcej pamięci podręcznej, gdy nie ma presji na pamięć.
1
Ale jeśli chodzi o drugą kwestię dotyczącą dużych obiektów, dokument MSDN stwierdza, że ​​chociaż długie słabe odwołania pozwalają na odtworzenie obiektu, jego stan pozostaje nieprzewidywalny. Jeśli zamierzasz za każdym razem odtwarzać go od zera, po co zawracać sobie głowę używaniem słabego odwołania, kiedy możesz po prostu wywołać funkcję / metodę, aby utworzyć ją na żądanie i zwrócić przejściową instancję?
Jest jedna sytuacja, w której buforowanie jest pomocne: jeśli często będziesz tworzyć niezmienne obiekty, z których wiele będzie identycznych (np. Odczyt wielu wierszy z pliku, który ma mieć wiele duplikatów), każdy ciąg zostanie utworzony jako nowy obiekt , ale jeśli wiersz pasuje do innego wiersza, do którego już istnieje odwołanie, wydajność pamięci można poprawić, jeśli nowe wystąpienie zostanie porzucone, a odniesienie do wcześniej istniejącego wystąpienia zostanie zastąpione. Zauważ, że ta zamiana jest przydatna, ponieważ i tak odbywa się inne odwołanie. Jeśli to nie był kod, należy zachować nowy.
supercat