2014-09-08 9 views
17

W jednym z projektów, w których biorę udział, jest szerokie zastosowanie WeakAction. Jest to klasa, która pozwala zachować referencję do instancji akcji, nie powodując, że jej cel nie zostanie zebrany. Sposób działania jest prosty, działa na konstruktorze i utrzymuje słabe odniesienie do celu akcji i metody, ale odrzuca odniesienie do samego działania. Kiedy nadejdzie czas wykonania akcji, sprawdza, czy cel wciąż żyje, a jeśli tak, wywołuje metodę na celu.Bug in WeakAction w przypadku zamknięcia działania

Wszystko działa dobrze, z wyjątkiem jednego przypadku - gdy akcja jest tworzona w zamknięciu. Rozważmy następujący przykład:

public class A 
{ 
    WeakAction action = null; 

    private void _register(string msg) 
    { 
     action = new WeakAction(() => 
     { 
      MessageBox.Show(msg); 
     } 
    } 
} 

Ponieważ wyrażenie lambda używa zmiennej lokalnej msg, auto C# kompilator generuje zagnieżdżone klasy, aby pomieścić wszystkie zmienne zamknięcia. Celem akcji jest instancja klasy zagnieżdżonej zamiast instancji A. Akcja przekazana do konstruktora WeakAction nie jest odwoływana po zakończeniu konstruktora, więc garbage collector może go natychmiast zlikwidować. Później, jeśli zostanie wykonany WeakAction, nie zadziała, ponieważ cel nie jest już aktywny, mimo że pierwotna instancja A jest żywa.

Nie mogę zmienić sposobu wywołania WeakAction (ponieważ jest szeroko stosowany), ale mogę zmienić jego implementację. Zastanawiałem się nad próbą znalezienia sposobu na uzyskanie dostępu do instancji A i zmuszenia instancji klasy zagnieżdżonej do pozostania przy życiu, podczas gdy instancja A jest wciąż żywa, ale nie wiem, jak ją uzyskać.

Istnieje wiele pytań o to, co A ma do czynienia z niczego, a sugestie, aby zmienić sposób A tworzy słabe działanie (których nie możemy zrobić), więc tutaj jest wyjaśnienie:

Instancja klasy A chce, aby instancja klasy B powiadomiła go, gdy coś się stanie, więc zapewnia wywołanie zwrotne za pomocą obiektu Action. A nie jest świadomy, że B używa słabych akcji, po prostu zapewnia Action, aby służyć jako callback. Fakt, że B używa WeakAction jest szczegółem implementacji, który nie jest ujawniony. B musi zapisać tę akcję i użyć jej w razie potrzeby. Ale B może żyć znacznie dłużej niż A, a posiadanie silnego odniesienia do normalnej akcji (która sama w sobie posiada silną referencję instancji A, która ją wygenerowała) powoduje, że A nigdy nie zostanie zbuforowane. Jeśli A jest częścią listy elementów, które nie są już żywe, oczekujemy, że A zostanie zbuforowane, a z powodu odniesienia do wstrzymań Akcji, która sama w sobie wskazuje na A, mamy wyciek pamięci.

Więc zamiast B posiadający działanie powodujące A warunkiem, B zawija je w WeakAction i przechowuje tylko słabe działanie. Kiedy nadejdzie czas, aby to nazwać, B robi to tylko wtedy, gdy WeakAction jest wciąż żywy, który powinien być tak długi, jak A jest wciąż żywy.

A tworzy tę akcję wewnątrz metody i nie zachowuje odniesienia do niej na własną rękę - to jest dane. Ponieważ Action został skonstruowany w kontekście konkretnej instancji A, to wystąpienie jest celem A, a gdy zginie A, wszystkie słabe odniesienia do niego stają się null, aby nie wywoływać go i unieszkodliwiać z obiektu .

Ale czasami metoda generująca Action wykorzystuje zmienne zdefiniowane lokalnie w tej funkcji. W takim przypadku kontekst, w którym działa akcja, obejmuje nie tylko instancję A, ale także stan zmiennych lokalnych w metodzie (nazywany "zamknięciem"). Kompilator C# robi to, tworząc ukrytą klasę zagnieżdżoną do przechowywania tych zmiennych (pozwala to nazwać ją A__closure), a wystąpienie, które staje się celem Action, jest instancją A__closure, a nie A. Jest to coś, o czym użytkownik nie powinien wiedzieć. Tyle tylko, że to wystąpienie A__closure odwołuje się tylko do obiektu Action. A ponieważ tworzymy słabe odniesienie do celu i nie mamy odniesienia do akcji, nie ma odniesienia do instancji A__closure, a garbage collector może (i zwykle robi) natychmiast ją pozbyć. Tak więc A umiera, A__closure umiera, i pomimo faktu, że A nadal oczekuje wywołania zwrotnego, nie można go wykonać podając B.

To błąd.

Moje pytanie było, czy ktoś zna sposób, że konstruktor WeakAction, jedyny kawałek kodu, który faktycznie posiada oryginalny przedmiot działania, tymczasowo, może w jakiś magiczny sposób wyodrębnić oryginalne wystąpienie A od instancji, że to A__closure znajduje się w Target z Action. Jeśli tak, mógłbym przedłużyć cykl życia A__Closure, aby dopasować go do stanu A.

+0

Czy zostało to naprawione w późniejszych wersjach C#? –

Odpowiedz

7

Po dalszych badaniach i zebraniu wszystkich przydatnych informacji z odpowiedzi zamieszczonych tutaj, zdałem sobie sprawę, że nie będzie eleganckiego i zapieczętowanego rozwiązania tego problemu. Ponieważ jest to problem z prawdziwego życia, podeszliśmy do pragmatycznego podejścia, starając się je przynajmniej ograniczyć, wykonując jak najwięcej scenariuszy, więc chciałem opublikować to, co zrobiliśmy.

Głębsze badanie obiektu Action przekazanego do konstruktora WeakEvent, a zwłaszcza właściwości Action.Target, wykazało, że istnieją efektywnie 2 różne przypadki obiektów zamknięcia.

Pierwszy przypadek dotyczy sytuacji, gdy Lambda używa zmiennych lokalnych z zakresu funkcji wywołującej, ale nie używa żadnych informacji z instancji klasy A. W poniższym przykładzie założono, że EventAggregator.Register jest metodą, która wykonuje akcję i przechowuje słabą funkcję, która ją opakowuje.

public class A 
{ 
    public void Listen(int num) 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction(num)); 
    } 

    public Action _createListenAction(int num) 
    { 
     return new Action(() => 
     { 
      if (num > 10) MessageBox.Show("This is a large number"); 
     }); 
    } 
} 

lambda utworzony, wykorzystuje zmienną num, która jest zmienną lokalną zdefiniowano w zakresie funkcji _createListenAction. Zatem kompilator musi zawinąć go klasą zamknięcia, aby zachować zmienne zamknięcia. Jednakże, ponieważ lambda nie ma dostępu do żadnego z elementów klasy A, nie ma potrzeby przechowywania odwołania do A. Cel akcji nie będzie zatem zawierał żadnego odniesienia do instancji A i nie ma absolutnie żadnego sposobu dla konstruktora WeakAction do niego dotrzeć.

W drugim przypadku jest zilustrowany na następującym przykładzie:

public class A 
{ 
    int _num = 10; 

    public void Listen() 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction()); 
    } 

    public Action _createListenAction() 
    { 
     return new Action(() => 
     { 
      if (_num > 10) MessageBox.Show("This is a large number"); 
     }); 
    } 
} 

teraz _num nie jest przewidziane jako parametrze, pochodzi z klasy instancji. Korzystanie z odbicia w celu poznania struktury obiektu Target ujawnia, że ​​ostatnie pole zdefiniowane przez kompilator zawiera odniesienie do instancji klasy A. Sprawa ta ma zastosowanie również wtedy, gdy lambda zawiera połączenia do metod członkowskich, jak w poniższym przykładzie:

public class A 
{ 
    private void _privateMethod() 
    { 
     // do something here 
    }   

    public void Listen() 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction()); 
    } 

    public Action _createListenAction() 
    { 
     return new Action(() => 
     { 
      _privateMethod(); 
     }); 
    } 
} 

_privateMethod jest funkcją członka, tak to się nazywa w kontekście klasy instancji, więc zamknięcie musi zachować odniesienie do niego w celu wywołania lambda we właściwym kontekście.

Pierwszy przypadek to zamknięcie, które zawiera tylko zmienną lokalną funkcji, a drugie zawiera odniesienie do instancji nadrzędnej A. W obu przypadkach nie ma żadnych twardych odniesień do instancji Closure, więc jeśli konstruktor WeakAction po prostu pozostawia rzeczy takimi, jakimi są, WeakAction "umrze" natychmiast, mimo że instancja klasy A wciąż żyje.

Mamy tu do czynienia z 3 różnymi problemami:

  1. Jak rozpoznać, że celem działania jest zagnieżdżona zamknięcie klasa instancji, a nie oryginalne Instancja?
  2. Jak uzyskać odwołanie do oryginalnej instancji klasy A?
  3. Jak przedłużyć żywotność instancji zamknięcia tak, aby działała ona tak długo, jak instancja A jest na żywo, ale nie poza nią?

Odpowiedź na pierwsze pytanie jest to, że możemy liczyć na 3 cech instancji Zamknięcie: - To jest prywatna (być bardziej dokładne, to nie jest „widoczny”.Podczas korzystania z kompilatora C#, odbitego typu ma IsPrivate ustawiony na true, ale z VB nie. We wszystkich przypadkach właściwość IsVisible ma wartość false). - Jest zagnieżdżony. - Jak wspomina @DarkFalcon w swojej odpowiedzi, jest on ozdobiony atrybutem [CompilerGenerated].

private static bool _isClosure(Action a) 
{ 
    var typ = a.Target.GetType(); 
    var isInvisible = !typ.IsVisible; 
    var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute)); 
    var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType; 


    return isNested && isCompilerGenerated && isInvisible; 
} 

Chociaż nie jest to w 100% szczelna orzecznik (złośliwy programista może wygenerować zagnieżdżone klasy prywatnej i udekoruj je z atrybutem CompilerGenerated), w realnych sytuacjach życiowych to jest na tyle dokładna, i znowu, budujemy pragmatyczne rozwiązanie, a nie akademickie.

Problem nr 1 został rozwiązany. Słaby konstruktor działania identyfikuje sytuacje, w których cel akcji jest zamknięciem i odpowiada na to.

Problem 3 jest również łatwy do rozwiązania. Jak napisał @usr w swojej odpowiedzi, gdy otrzymamy blok instancji klasy A, dodanie warunkowej konfiguracji z pojedynczym wpisem, w którym instancja klasy A jest kluczem, a wystąpienie zamknięcia jest celem, rozwiązuje problem. Śmieciarz wie, że nie będzie zbierać instancji zamknięcia, dopóki istnieje instancja klasy A. Więc to jest w porządku.

Jedyny nierozwiązywalny problem to drugi, jak uzyskać odwołanie do instancji klasy A? Tak jak powiedziałem, istnieją 2 przypadki zamknięcia. Jeden, w którym kompilator tworzy element, który przechowuje tę instancję, i taki, w którym nie działa. W drugim przypadku po prostu nie ma sposobu, aby to uzyskać, więc jedyną rzeczą, którą możemy zrobić, to stworzyć twarde odwołanie do instancji zamknięcia, aby zachować ją od natychmiastowego zbierania śmieci. Oznacza to, że może żyć na żywo w instancji klasy A (w rzeczywistości będzie działać tak długo, jak długo trwa instancja WeakAction, która może być na zawsze). Ale to wcale nie jest taka straszna sprawa. Klasa zamknięcia w tym przypadku zawiera tylko kilka zmiennych lokalnych, aw 99,9% przypadków jest to bardzo mała struktura. Chociaż jest to nadal przeciek pamięci, nie jest on znaczący.

Ale tylko w celu umożliwienia użytkownikom uniknąć nawet to wyciek pamięci, my dodaliśmy dodatkowy konstruktora klasy WeakAction, co następuje:

public WeakAction(object target, Action action) {...} 

A kiedy ten konstruktor nazywa, dodamy Warunek ConditionalWeakTable, w którym celem jest klucz, a cel działań jest wartością. Mamy również słabe odniesienie zarówno do celu, jak i celu akcji, a jeśli którykolwiek z nich umrze, usuwamy oba. Tak więc cel działania nie może być mniejszy niż cel podany. W zasadzie pozwala to użytkownikowi WeakAction powiedzieć mu, aby trzymał się instancji zamknięcia tak długo, jak cel żyje. Tak więc nowi użytkownicy zostaną poinformowani o konieczności użycia go w celu uniknięcia wycieków pamięci. Ale w istniejących projektach, w których ten nowy konstruktor nie jest używany, minimalizuje to co najmniej wycieki pamięci do zamknięć, które nie mają odniesienia do instancji klasy A.

Przypadki zamknięcia, które odnoszą się do obiektu nadrzędnego, są bardziej problematyczne, ponieważ mają wpływ na kolekcję kolekcji. Jeśli utrzymamy twarde odwołanie do zamknięcia, spowodujemy o wiele bardziej drastyczny wyciek pamięci, ponieważ instancja klasy A również nigdy nie zostanie wyczyszczona. Ale ta sprawa jest również łatwiejsza w leczeniu. Ponieważ kompilator dodaje ostatni element, który zawiera odwołanie do instancji klasy A, po prostu używamy odbicia, aby wyodrębnić i wykonać dokładnie to, co robimy, gdy użytkownik dostarcza je w konstruktorze. Identyfikujemy ten przypadek, gdy ostatni element instancji zamknięcia jest tego samego typu co typ deklarujący klasy zagnieżdżonej zamknięcia. (Ponownie, nie jest to w 100% dokładne, ale w rzeczywistych przypadkach jest wystarczająco blisko).

Podsumowując, przedstawione tutaj rozwiązanie nie jest w 100% zamknięte, po prostu dlatego, że nie istnieje takie rozwiązanie. Ale ponieważ musimy podać NIEKTÓRE odpowiedzi na ten irytujący błąd, to rozwiązanie to przynajmniej znacznie zmniejsza problem.

2

zapewnia dostęp do obiektu, który przechowuje parametry lambda. Wykonanie w tym celu GetType spowoduje zwrócenie typu generowanego przez kompilator. Jedną z opcji byłoby sprawdzenie tego typu dla niestandardowego atrybutu System.Runtime.CompilerServices.CompilerGeneratedAttribute i zachowanie silnego odniesienia do obiektu w tym przypadku.

Now I can't change the way the WeakAction is called, (since it's in wide use) but I can change it's implementation. Należy pamiętać, że jest to jedyny sposób, który umożliwia utrzymanie go przy życiu bez konieczności modyfikacji sposobu konstruowania modelu WeakAction. Nie osiąga również celu utrzymywania lambda przy życiu tak długo, jak obiekt A (utrzyma go przy życiu tak długo, jak długo będzie to WeakAction). Nie wierzę, że będzie to możliwe bez zmiany sposobu konstruowania modelu WeakAction, tak jak ma to miejsce w przypadku innych odpowiedzi. Co najmniej WeakAction musi uzyskać odwołanie do obiektu A, którego obecnie nie udostępniasz.

+0

Rzeczywiście obiekt docelowy ma atrybut generowany przez kompilator, ale jeśli utrzymam silne odniesienie do niego, mogę spowodować wycieki pamięci. Na przykład, jeśli akcja wywołuje metody z A, to faktycznie zachowuje odniesienie do instancji A, a wtedy ta instancja nigdy nie będzie zbierała śmieci. –

+0

Wtedy nie masz wyboru, MUSISZ zmienić sposób konstruowania 'WeakAction'. Myślę, że "ConditionalWeakTable" jest dobrym podejściem (Make 'WeakAction' użyj jednego.) –

+0

Jest to dobre podejście do przyszłych zastosowań, ale po prostu MUSI wspierać odwrotne użycie słabych działań bez zmian w podpisie. W przeciwnym razie nie jest to dopuszczalne rozwiązanie. –

5

Chcesz przedłużyć okres istnienia instancji klasy zamknięcia, aby był dokładnie taki sam, jaki ma instancja A. CLR ma specjalny typ uchwytu GC: the Ephemeron, zaimplementowany jako internal struct DependentHandle.

  1. Ta struktura jest dostępna tylko jako część klasy ConditionalWeakTable. Możesz utworzyć jedną taką tabelę na WeakAction z dokładnie jedną pozycją. Klucz będzie instancją A, wartość będzie instancją klasy zamknięcia.
  2. Ewentualnie możesz podważyć DependentHandle używając prywatnego odbicia.
  3. Można też użyć jednej współużytkowanej na całym świecie instancji ConditionalWeakTable. Prawdopodobnie wymaga użycia synchronizacji. Spójrz na dokumenty.

Rozważ otwarcie problemu z łączeniem, aby publicznie udostępnić numer DependentHandle i podać link do tego pytania, aby zapewnić zastosowanie.

+0

dziękuję, wszystkie te wydają się świetne sugestie, z tym wyjątkiem, że nie wiem, jak uzyskać wstrzymanie instancji A. Czy istnieje sposób na to, aby zagnieździć klasy? –

+0

@KobiHari można wziąć zależność od wewnętrznych komponentów kompilatora C# i wyodrębnić odwołanie z zamknięcia za pomocą odbicia. Dekompiluj część zestawu, aby dowiedzieć się, jak generują nazwy; Lepiej: spraw, aby wywoływacz "nowej WeakAction" przechodził w tablicy elementów, które chce używać jako "GC roots". Osoba dzwoniąca musi przekazać słowo "A". – usr

+0

Chciałbym usłyszeć więcej szczegółów na temat pierwszej opcji. Co do drugiego, nie mogę zmienić sygnatury słabych akcji. –