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_ptr
zostaną 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?
Odpowiedzi:
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 naOpen()
/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.źródło
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ą, aIDisposable
wdrożenie jest najmniej krytyczny.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.
źródło
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
using
iIDispose
, 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
IDispose
iusing
konstrukcji przepływu sterowania) mogą z kolei informować o większym obiekcie współdzielonym (być może niestandardowymAcquire
iRelease
metodach).( Przedstawione poniżej metody
Acquire
iRelease
są również dostępne poza konstrukcją używającą, ale bez bezpieczeństwatry
ukrytego wusing
.)Przykład w C #
źródło
IDisposable.Dispose
państw, które dzwoniąDispose
wiele razy na ten sam obiekt, musi być zakazem. Gdybym miał zaimplementować taki wzorzec, ustawiłbym równieżRelease
prywatny, aby uniknąć niepotrzebnych błędów i zamiast delegować dziedziczenie użyć delegacji (usunąć interfejs, zapewnić prostąSharedDisposable
klasę, której można używać z dowolnymi jednorazowymi urządzeniami jednorazowymi), ale są to bardziej kwestie gustu.Zdecydowana większość obiektów w systemie powinna zasadniczo pasować do jednego z trzech wzorów:
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.
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.
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.
źródło
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=ownership
jednak 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:
... 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