Wzorzec zliczania odniesienia dla języków zarządzanych w pamięci?

11

Java i .NET mają wspaniałe kolektory śmieci, które zarządzają pamięcią dla Ciebie, i wygodne wzorce do szybkiego uwalniania obiektów zewnętrznych ( Closeable, IDisposable), ale tylko jeśli są własnością jednego obiektu. W niektórych systemach zasób może wymagać niezależnego zużycia przez dwa składniki i może zostać zwolniony tylko wtedy, gdy oba składniki zwolnią zasób.

We współczesnym C ++ rozwiązałbyś ten problem za pomocą shared_ptr, który deterministycznie uwolniłby zasób, gdy wszystkie shared_ptrzostaną zniszczone.

Czy istnieją udokumentowane, sprawdzone wzorce zarządzania i zwalniania drogich zasobów, które nie mają jednego właściciela w obiektowych, niedeterministycznie usuwanych systemach?

Krzyż
źródło
1
Czy widziałeś automatyczne liczenie referencji Clanga , również używane w Swift ?
jscs,
1
@JoshCaswell Tak, i to rozwiązałoby problem, ale pracuję w przestrzeni na śmieci.
C. Ross
8
Liczenie referencji to strategia czyszczenia pamięci.
Jörg W Mittag,

Odpowiedzi:

15

Ogólnie rzecz biorąc, unikasz tego, mając jednego właściciela - nawet w niezarządzanych językach.

Ale zasada jest taka sama dla zarządzanych języków. Zamiast natychmiast zamykać kosztowny zasób na Close()zmniejszającym się liczniku (zwiększanym na Open()/ Connect()/ itd.), Dopóki nie osiągniesz 0, w którym momencie zamknięcie faktycznie wykonuje zamknięcie. Prawdopodobnie będzie wyglądać i działać jak wzór Flyweight.

Telastyn
źródło
Tak też myślałem, ale czy istnieje na to udokumentowany wzór? Flyweight jest z pewnością podobny, ale specjalnie dla pamięci, jak to zwykle definiuje się.
C. Ross
@ C.Ross Wygląda na to, że zachęca się finalistów. Możesz użyć klasy opakowania wokół niezarządzanego zasobu, dodając do tej klasy finalizator w celu zwolnienia zasobu. Możesz go również wdrożyć IDisposable, zliczać, aby jak najszybciej zwolnić zasób itp. Prawdopodobnie najlepszą rzeczą, często, jest posiadanie wszystkich trzech, ale finalizator jest prawdopodobnie najbardziej krytyczną częścią, a IDisposablewdrożenie jest najmniej krytyczny.
Panzercrisis,
11
@Panzercrisis z wyjątkiem tego, że finalizatory nie są gwarantowane do uruchomienia, a zwłaszcza nie do zagwarantowania, że ​​zostaną uruchomione szybko .
Caleth,
@ Caleth Myślałem, że liczenie pomoże w części dotyczącej terminowości. Jeśli chodzi o to, że wcale nie działają, czy masz na myśli, że CLR może po prostu nie obejść się przed zakończeniem programu, czy masz na myśli, że mogą zostać zdyskwalifikowani?
Panzercrisis,
14

W języku odśmiecania pamięci (gdzie GC nie jest deterministyczne), nie jest możliwe niezawodne powiązanie czyszczenia zasobu innego niż pamięć z czasem życia obiektu: Nie jest możliwe określenie, kiedy obiekt zostanie usunięty. Koniec życia zależy wyłącznie od śmieciarza. GC gwarantuje tylko, że obiekt będzie żył, dopóki będzie osiągalny. Gdy obiekt stanie się nieosiągalny, może zostać wyczyszczony w pewnym momencie w przyszłości, co może wymagać uruchomienia finalizatorów.

Pojęcie „własności zasobów” tak naprawdę nie ma zastosowania w języku GC. System GC jest właścicielem wszystkich obiektów.

Co te języki oferują z try-with-resource + Closeable (Java), za pomocą instrukcji + IDisposable (C #) lub z instrukcjami + menedżerami kontekstu (Python) jest sposobem na przepływ kontroli (! = Obiekty) do przechowywania zasobu, który jest zamykany, gdy przepływ kontrolny opuszcza zakres. We wszystkich tych przypadkach jest to podobne do wstawionego automatycznie try { ... } finally { resource.close(); }. Czas życia obiektu reprezentującego zasób nie jest związany z czasem życia zasobu: obiekt może nadal żyć po zamknięciu zasobu, a obiekt może stać się nieosiągalny, gdy zasób jest nadal otwarty.

W przypadku zmiennych lokalnych podejścia te są równoważne RAII, ale muszą być stosowane jawnie w witrynie wywołującej (w przeciwieństwie do destruktorów C ++, które będą działać domyślnie). Dobre IDE ostrzeże, gdy zostanie to pominięte.

Nie działa to w przypadku obiektów, do których istnieją odniesienia z lokalizacji innych niż zmienne lokalne. Tutaj nie ma znaczenia, czy istnieje jedno, czy więcej odniesień. Możliwe jest przetłumaczenie odwołań do zasobów za pomocą odwołań do obiektów na własność zasobów poprzez przepływ sterowania poprzez utworzenie osobnego wątku, który przechowuje ten zasób, ale wątki również są zasobami, które należy usunąć ręcznie.

W niektórych przypadkach możliwe jest przekazanie własności zasobów do funkcji wywołującej. Zamiast tymczasowych obiektów odwołujących się do zasobów, które powinny (ale nie mogą) wyczyścić niezawodnie, funkcja wywołująca zawiera zestaw zasobów, które należy wyczyścić. Działa to tylko do momentu, gdy czas życia któregokolwiek z tych obiektów przeżywa okres istnienia funkcji, a zatem odwołuje się do zasobu, który został już zamknięty. Kompilator nie może tego wykryć, chyba że język ma śledzenie własności podobne do Rust (w takim przypadku istnieją już lepsze rozwiązania tego problemu zarządzania zasobami).

Pozostaje to jedynym realnym rozwiązaniem: ręczne zarządzanie zasobami, być może poprzez samodzielne wdrożenie liczenia referencji. Jest to podatne na błędy, ale nie niemożliwe. W szczególności konieczność myślenia o własności jest czymś niezwykłym w językach GC, więc istniejący kod może nie być wystarczająco wyraźny na temat gwarancji własności.

amon
źródło
3

Wiele dobrych informacji z innych odpowiedzi.

Nadal jednak, wzorzec, którego możesz szukać, to to, że używasz małych pojedynczych obiektów do konstrukcji przepływu kontrolnego podobnego do RAII za pośrednictwem usingi IDispose, w połączeniu z (większym, prawdopodobnie liczonym odniesieniem) obiektem, który przechowuje niektóre (działające system) zasoby.

Istnieją więc małe nieudostępnione obiekty jednego właściciela, które (za pomocą obiektu mniejszego IDisposei usingkonstrukcji przepływu sterowania) mogą z kolei informować o większym obiekcie współdzielonym (być może niestandardowym Acquirei Releasemetodach).

( Przedstawione poniżej metody Acquirei Releasesą również dostępne poza konstrukcją używającą, ale bez bezpieczeństwa tryukrytego w using.)


Przykład w 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;
    }
}
Erik Eidt
źródło
Jeśli powinno to być C # (tak to wygląda), to twoja implementacja Reference <T> jest subtelnie niepoprawna. Umowa dla IDisposable.Disposepaństw, które dzwonią Disposewiele razy na ten sam obiekt, musi być zakazem. Gdybym miał zaimplementować taki wzorzec, ustawiłbym również Releaseprywatny, aby uniknąć niepotrzebnych błędów i zamiast delegować dziedziczenie użyć delegacji (usunąć interfejs, zapewnić prostą SharedDisposableklasę, której można używać z dowolnymi jednorazowymi urządzeniami jednorazowymi), ale są to bardziej kwestie gustu.
Voo,
@Voo, ok, dobra uwaga, dzięki!
Erik Eidt,
1

Zdecydowana większość obiektów w systemie powinna zasadniczo pasować do jednego z trzech wzorów:

  1. Obiekty, których stan nigdy się nie zmieni i do których odniesienia są utrzymywane wyłącznie jako sposób enkapsulacji stanu. Jednostki, które posiadają referencje, nie wiedzą ani nie dbają o to, czy jakiekolwiek inne jednostki posiadają referencje do tego samego obiektu.

  2. Obiekty znajdujące się pod wyłączną kontrolą pojedynczego bytu, który jest wyłącznym właścicielem wszystkich stanów w nim, i wykorzystuje obiekt wyłącznie jako środek do enkapsulacji (ewentualnie zmiennego) stanu w nim.

  3. Obiekty będące własnością jednego podmiotu, ale z których inne podmioty mogą korzystać w ograniczony sposób. Właściciel obiektu może używać go nie tylko jako sposobu enkapsulacji stanu, ale także enkapsulacji relacji z innymi podmiotami, które go udostępniają.

Śledzenie wyrzucania elementów bezużytecznych działa lepiej niż liczenie referencji dla nr 1, ponieważ kod korzystający z takich obiektów nie musi robić nic specjalnego, gdy jest wykonywany z ostatnim pozostałym odwołaniem. Zliczanie referencji nie jest potrzebne dla nr 2, ponieważ obiekty będą miały dokładnie jednego właściciela i będą wiedzieć, kiedy obiekt nie będzie już potrzebny. Scenariusz # 3 może stanowić pewną trudność, jeśli właściciel obiektu zabije go, podczas gdy inne podmioty nadal będą mieć odniesienia; nawet tam śledząca GC może być lepsza niż zliczanie odniesień w zapewnianiu, że odniesienia do martwych obiektów pozostają niezawodnie identyfikowalne jako odniesienia do martwych obiektów, dopóki istnieją takie odniesienia.

Istnieje kilka sytuacji, w których może być konieczne pozyskanie i przechowywanie obiektów zewnętrznych bez właściciela, o ile ktoś potrzebuje ich usług, i powinien je zwolnić, gdy jego usługi nie są już potrzebne. Na przykład obiekt, który obudowuje zawartość pliku tylko do odczytu, może być współużytkowany i używany przez wiele podmiotów jednocześnie, bez potrzeby, aby którykolwiek z nich wiedział o istnieniu lub dbał o siebie. Takie okoliczności są jednak rzadkie. Większość obiektów będzie miała jednego wyraźnego właściciela, albo będzie pozbawiona właściciela. Wielokrotna własność jest możliwa, ale rzadko przydatna.

supercat
źródło
0

Współwłasność rzadko ma sens

Ta odpowiedź może być nieco styczna, ale muszę zapytać, ile przypadków ma sens z punktu widzenia użytkownika, aby podzielić się własnością ? Przynajmniej w domenach, w których pracowałem, praktycznie nie było żadnych, ponieważ w przeciwnym razie oznaczałoby to, że użytkownik nie musiałby po prostu usunąć czegoś raz z jednego miejsca, ale jawnie usunąć go od wszystkich odpowiednich właścicieli, zanim zasób faktycznie usunięty z systemu.

Często jest to pomysł inżynieryjny na niższym poziomie, aby zapobiec zniszczeniu zasobów, gdy coś jeszcze ma do nich dostęp, na przykład inny wątek. Często, gdy użytkownik prosi o zamknięcie / usunięcie / usunięcie czegoś z oprogramowania, należy je usunąć tak szybko, jak to możliwe (zawsze, gdy można je bezpiecznie usunąć), a na pewno nie powinno pozostać w pobliżu i powodować wycieku zasobów tak długo, jak to możliwe aplikacja jest uruchomiona.

Na przykład zasób gry w grze wideo może odwoływać się do materiału z biblioteki materiałów. Z pewnością nie chcemy, powiedzmy, zawieszającego się wskaźnika, jeśli materiał zostanie usunięty z biblioteki materiałów w jednym wątku, podczas gdy inny wątek nadal uzyskuje dostęp do materiału, do którego odnosi się zasób gry. Ale to nie znaczy, że zasoby gry nie mają sensu współdzielić własności materiałów, do których się odnoszą, z biblioteką materiałów. Nie chcemy zmusić użytkownika do jawnego usunięcia materiału z zasobów i biblioteki materiałów. Chcemy tylko upewnić się, że materiały nie zostaną usunięte z biblioteki materiałów, jedynego rozsądnego właściciela materiałów, dopóki inne wątki nie zakończą uzyskiwania dostępu do materiału.

Wycieki zasobów

Jednak współpracowałem z byłym zespołem, który objął GC za wszystkie komponenty oprogramowania. I chociaż to naprawdę pomogło upewnić się, że nigdy nie zniszczyliśmy zasobów, podczas gdy inne wątki nadal miały do ​​nich dostęp, zamiast tego dostaliśmy naszą część wycieków zasobów .

Nie były to trywialne wycieki zasobów, które denerwują tylko programistów, jak kilobajt pamięci wyciekły po godzinnej sesji. Były to epickie wycieki, często gigabajty pamięci podczas aktywnej sesji, prowadzące do zgłoszeń błędów. Ponieważ teraz, gdy odwołanie do własności zasobu (a zatem współwłasności) między, powiedzmy, 8 różnymi częściami systemu, wystarczy, że jedna osoba nie usunie zasobu w odpowiedzi na użytkownika żądającego jego usunięcia wyciek i być może na czas nieokreślony.

Więc nigdy nie byłem wielkim fanem GC lub liczenia referencji stosowanych na szeroką skalę ze względu na to, jak łatwo stworzyli nieszczelne oprogramowanie. To, co poprzednio było zwisającą katastrofą wskaźnika, łatwą do wykrycia, zmienia się w bardzo trudny do wykrycia wyciek zasobów, który z łatwością może latać pod radarem testowania.

Słabe referencje mogą złagodzić ten problem, jeśli zapewnia je język / biblioteka, ale trudno mi było zdobyć zespół programistów o mieszanych umiejętnościach, aby móc konsekwentnie używać słabych referencji, gdy jest to właściwe. Trudność ta dotyczyła nie tylko wewnętrznego zespołu, ale każdego twórcy wtyczek do naszego oprogramowania. Oni również mogą łatwo spowodować wyciek zasobów z systemu poprzez przechowywanie trwałego odwołania do obiektu w sposób, który utrudniał odnalezienie wtyczki jako winowajcy, więc otrzymaliśmy również dużą część raportów o błędach wynikających z zasobów oprogramowania wyciekł po prostu dlatego, że wtyczka, której kod źródłowy był poza naszą kontrolą, nie opublikowała odniesień do tych drogich zasobów.

Rozwiązanie: odroczone, okresowe usuwanie

Więc moim późniejszym rozwiązaniem, które zastosowałem do moich osobistych projektów, które dało mi to, co najlepsze z obu światów, było wyeliminowanie koncepcji, która referencing=ownershipjednak odroczyła niszczenie zasobów.

W rezultacie teraz, ilekroć użytkownik robi coś, co powoduje, że zasób wymaga usunięcia, interfejs API jest wyrażany w kategoriach samego usunięcia zasobu:

ecs->remove(component);

... który modeluje logikę użytkownika w bardzo prosty sposób. Jednak zasób (komponent) nie może zostać usunięty od razu, jeśli w fazie przetwarzania znajdują się inne wątki systemowe, w których mogą uzyskiwać dostęp do tego samego komponentu jednocześnie.

Te wątki przetwarzania dają więc czas tu i tam, co pozwala wątkowi przypominającemu śmieciarz obudzić się i „ zatrzymać świat ” i zniszczyć wszystkie zasoby, których usunięcia zażądano, blokując wątki przed przetwarzaniem tych składników, aż do jego zakończenia. . Dostosowałem to tak, aby ilość pracy, którą trzeba tu wykonać, jest na ogół minimalna i nie zmniejsza zauważalnie liczby klatek na sekundę.

Teraz nie mogę powiedzieć, że jest to jakaś wypróbowana i przetestowana metoda, ale używam jej od kilku lat bez żadnych problemów i żadnych wycieków zasobów. Polecam zbadanie takich podejść, gdy jest to możliwe, aby Twoja architektura pasowała do tego rodzaju modelu współbieżności, ponieważ jest znacznie mniej obciążona niż GC lub liczenie ref i nie ryzykuje tego rodzaju wycieków zasobów latających pod radarem testowania.

Jedynym miejscem, w którym uważam, że przydatne jest liczenie odwołań lub GC, są trwałe struktury danych. W takim przypadku jest to terytorium struktury danych, dalekie od obaw użytkowników, i tam naprawdę niezmienne jest, aby każda niezmienna kopia potencjalnie współdzieliła własność tych samych niezmodyfikowanych danych.


źródło