Dlaczego wyrzucanie elementów bezużytecznych obejmuje tylko pamięć, a nie inne typy zasobów?

12

Wygląda na to, że ludzie zmęczyli się ręcznym zarządzaniem pamięcią, więc wymyślili zbieranie śmieci, a życie było w miarę dobre. Ale co z każdym innym rodzajem zasobów? Deskryptory plików, gniazda, a nawet dane tworzone przez użytkownika, takie jak połączenia z bazą danych?

To naiwne pytanie, ale nie mogę znaleźć miejsca, w którym ktoś by je zadał. Rozważmy deskryptory plików. Załóżmy, że program wie, że po uruchomieniu będzie on mógł mieć dostęp tylko 4000 fds. Ilekroć wykonuje operację, która otworzy deskryptor pliku, co jeśli by to zrobił

  1. Sprawdź, czy nie zabraknie.
  2. Jeśli tak, uruchom moduł wyrzucający elementy bezużyteczne, który zwolni sporo pamięci.
  3. Jeśli część zwolnionej pamięci zawierała odwołania do deskryptorów plików, natychmiast je zamknij. Wie, że pamięć należała do zasobu, ponieważ pamięć powiązana z tym zasobem została zarejestrowana w „rejestrze deskryptorów plików”, z braku lepszego terminu, przy pierwszym otwarciu.
  4. Otwórz nowy deskryptor pliku, skopiuj go do nowej pamięci, zarejestruj tę lokalizację pamięci w „rejestrze deskryptorów plików” i zwróć użytkownikowi.

Tak więc zasób nie zostanie natychmiast zwolniony, ale zostanie uwolniony za każdym razem, gdy uruchomi się gc, co obejmuje przynajmniej tuż przed wyczerpaniem się zasobu, zakładając, że nie jest on w pełni wykorzystywany.

I wydaje się, że byłoby to wystarczające w przypadku wielu problemów z czyszczeniem zasobów zdefiniowanych przez użytkownika. Udało mi się znaleźć tutaj pojedynczy komentarz, który odwołuje się do czyszczenia podobnie jak w C ++ z wątkiem, który zawiera odwołanie do zasobu i czyści go, gdy pozostało tylko jedno odwołanie (z wątku czyszczenia), ale mogę znaleźć żadnych dowodów na to, że jest to biblioteka lub część dowolnego istniejącego języka.

osoba czytająca w myślach
źródło

Odpowiedzi:

4

GC zajmuje się przewidywalnym i zarezerwowanym zasobem. Maszyna wirtualna ma nad nią całkowitą kontrolę i całkowitą kontrolę nad tym, jakie wystąpienia są tworzone i kiedy. Słowa kluczowe są tutaj „zastrzeżone” i „pełna kontrola”. Uchwyty są przydzielane przez system operacyjny, a wskaźniki są ... dobrze wskaźnikami do zasobów przydzielonych poza zarządzanym miejscem. Z tego powodu uchwyty i wskaźniki nie są ograniczone do użycia wewnątrz kodu zarządzanego. Mogą być używane - i często są - przez zarządzany i niezarządzany kod działający w tym samym procesie.

„Kolektor zasobów” byłby w stanie zweryfikować, czy uchwyt / wskaźnik jest używany w zarządzanej przestrzeni, czy nie, ale z definicji nie jest świadomy tego, co dzieje się poza przestrzenią pamięci (i, co gorsza, niektóre uchwyty mogą być używane ponad granicami procesu).

Praktycznym przykładem jest .NET CLR. Można używać aromatyzowanego C ++ do pisania kodu, który działa zarówno z zarządzanymi, jak i niezarządzanymi obszarami pamięci; uchwyty, wskaźniki i referencje mogą być przekazywane między kodem zarządzanym i niezarządzanym. Kod niezarządzany musi używać specjalnych konstrukcji / typów, aby umożliwić CLR śledzenie odniesień do zarządzanych zasobów. Ale to najlepsze, co może zrobić. Nie może zrobić tego samego z uchwytami i wskaźnikami, dlatego wspomniany moduł zbierający zasoby nie będzie wiedział, czy można zwolnić określony uchwyt lub wskaźnik.

edycja: Jeśli chodzi o .NET CLR, nie mam doświadczenia w tworzeniu C ++ na platformie .NET. Być może istnieją specjalne mechanizmy, które pozwalają CLR śledzić odwołania do uchwytów / wskaźników między kodem zarządzanym i niezarządzanym. W takim przypadku CLR może zadbać o żywotność tych zasobów i zwolnić je, gdy wszystkie odniesienia do nich zostaną usunięte (cóż, przynajmniej w niektórych scenariuszach może to zrobić). Tak czy inaczej, najlepsze praktyki określają, że uchwyty (szczególnie te wskazujące na pliki) i wskaźniki powinny zostać zwolnione, gdy tylko nie będą potrzebne. Kolektor zasobów nie przestrzegałby tego, to kolejny powód, aby go nie mieć.

edycja 2: Na CLR / JVM / VMs-ogólnie ogólnie jest dość trywialne napisanie kodu, aby zwolnić określony uchwyt, jeśli jest używany tylko w zarządzanej przestrzeni. W .NET byłoby coś takiego:

// This class offends many best practices, but it would do the job.
public class AutoReleaseFileHandle {
    // keeps track of how many instances of this class is in memory
    private static int _toBeReleased = 0;

    // the threshold when a garbage collection should be forced
    private const int MAX_FILES = 100;

    public AutoReleaseFileHandle(FileStream fileStream) {
       // Force garbage collection if max files are reached.
       if (_toBeReleased >= MAX_FILES) {
          GC.Collect();
       }
       // increment counter
       Interlocked.Increment(ref _toBeReleased);
       FileStream = fileStream;
    }

    public FileStream { get; private set; }

    private void ReleaseFileStream(FileStream fs) {
       // decrement counter
       Interlocked.Decrement(ref _toBeReleased);
       FileStream.Close();
       FileStream.Dispose();
       FileStream = null;
    }

    // Close and Dispose the Stream when this class is collected by the GC.
    ~AutoReleaseFileHandle() {
       ReleaseFileStream(FileStream);
    }

    // because it's .NET this class should also implement IDisposable
    // to allow the user to dispose the resources imperatively if s/he wants 
    // to.
    private bool _disposed = false;
    public void Dispose() {
      if (_disposed) {
        return;
      }
      _disposed = true;
      // tells GC to not call the finalizer for this instance.
      GC.SupressFinalizer(this);

      ReleaseFileStream(FileStream);
    }
}

// use it
// for it to work, fs.Dispose() should not be called directly,
var fs = File.Open("path/to/file"); 
var autoRelease = new AutoReleaseFileHandle(fs);
Marcelo De Zen
źródło
3

To wydaje się być jednym z powodów, dla których języki z modułami odśmiecającymi implementują finalizatory. Finalizatory mają na celu umożliwić programiście czyszczenie zasobów obiektu podczas wyrzucania elementów bezużytecznych. Dużym problemem związanym z finalizatorami jest to, że nie gwarantuje się ich uruchomienia.

Tutaj jest całkiem niezły opis używania finalizatorów:

Finalizacja i czyszczenie obiektu

W rzeczywistości używa jako przykładu deskryptora pliku. Powinieneś upewnić się, że samodzielnie wyczyścisz taki zasób, ale istnieje mechanizm, który MOŻE przywrócić zasoby, które nie zostały prawidłowo zwolnione.

Brian Hibbert
źródło
Nie jestem pewien, czy to odpowiada na moje pytanie. Brakuje mi części mojej propozycji, w której system wie, że niedługo skończy mu się zasób. Jedynym sposobem na wbicie tej części jest upewnienie się, że ręcznie uruchomisz gc przed przydzieleniem nowych deskryptorów plików, ale jest to wyjątkowo nieefektywne i nie wiem, czy możesz nawet spowodować uruchomienie gc w Javie.
mindreader
OK, ale deskryptory plików zazwyczaj reprezentują otwarty plik w systemie operacyjnym, co oznacza (w zależności od systemu operacyjnego), że używa zasobów na poziomie systemu, takich jak blokady, pule buforów, pule struktur itp. Szczerze mówiąc, nie widzę korzyści z pozostawienia tych struktur otwartych do późniejszego wyrzucania elementów bezużytecznych i widzę wiele przeszkód w pozostawieniu ich przydzielonych dłużej niż to konieczne. Metody Finalize () mają na celu umożliwienie ostatniego czyszczenia rowu w przypadku, gdy programista przeoczył wywołania w celu oczyszczenia zasobów, ale nie należy na nim polegać.
Brian Hibbert
Rozumiem, że powód, dla którego nie należy na nich polegać, to fakt, że jeśli przydzielisz tonę tych zasobów, na przykład schodząc w dół hierarchii plików otwierając każdy plik, możesz otworzyć zbyt wiele plików, zanim zdarzy się gc uciekaj, powodując wybuch. To samo stanie się z pamięcią, z tym wyjątkiem, że środowisko wykonawcze sprawdza, czy nie zabraknie pamięci. Chciałbym wiedzieć, dlaczego nie można wdrożyć systemu w celu odzyskania dowolnych zasobów przed powiększeniem, w taki sam sposób jak pamięć.
mindreader
System MUSI zostać zapisany w zasobach GC innych niż pamięć, ale trzeba będzie śledzić liczbę referencji lub zastosować inną metodę określania, kiedy zasób nie jest już używany. NIE chcesz cofnąć przydziału i ponownie przydzielić zasobów, które są nadal w użyciu. Cały dwór chaosu może powstać, jeśli w wątku jest otwarty plik do zapisu, system operacyjny „odzyskuje” uchwyt pliku, a inny wątek otwiera inny plik do zapisu przy użyciu tego samego uchwytu. I nadal sugerowałbym, że marnowanie znacznych zasobów, aby pozostawić je otwarte, dopóki wątek podobny do GC nie pojawi się, by je uwolnić.
Brian Hibbert
3

Istnieje wiele technik programowania, które pomagają zarządzać tego rodzaju zasobami.

  • Programiści C ++ często używają wzorca o nazwie Resource Acquisition to Inicjalizacja , w skrócie RAII. Ten wzorzec gwarantuje, że gdy obiekt trzymający zasoby wyjdzie poza zakres, zamknie zasoby, które trzymał. Jest to pomocne, gdy czas życia obiektu odpowiada określonemu zakresowi w programie (np. Gdy pasuje do czasu, w którym na stosie znajduje się określona ramka stosu), więc jest pomocny w przypadku obiektów wskazywanych przez zmienne lokalne (wskaźnik zmienne przechowywane na stosie), ale nie tak pomocne dla obiektów wskazywanych przez wskaźniki przechowywane na stercie.

  • Java, C # i wiele innych języków umożliwiają określenie metody, która będzie wywoływana, gdy obiekt nie będzie już żywy i ma zostać odebrany przez moduł wyrzucający elementy bezużyteczne. Zobacz np. Finalizatory dispose()i inne. Chodzi o to, że programista może zaimplementować taką metodę, aby jawnie zamknęła zasób, zanim obiekt zostanie zwolniony przez moduł wyrzucający elementy bezużyteczne. Jednak w tych podejściach występują pewne problemy, o których można przeczytać gdzie indziej; na przykład śmieciarz może nie odebrać obiektu, dopóki nie będzie to konieczne.

  • C # i inne języki zapewniają usingsłowo kluczowe, które pomaga zapewnić zamknięcie zasobów, gdy nie są już potrzebne (więc nie zapomnij zamknąć deskryptora pliku lub innego zasobu). Jest to często lepsze niż poleganie na śmieciarzu w celu wykrycia, że ​​obiekt nie jest już aktywny. Zobacz np . Https://stackoverflow.com/q/75401/781723 . Ogólny termin tutaj to zasób zarządzany . Pojęcie to opiera się na RAII i finalizatorach, poprawiając je pod pewnymi względami.

DW
źródło
Mniej mnie interesuje szybka dezalokacja zasobów, a bardziej interesuje mnie idea delokalizacji na czas. RIAA jest świetna, ale nie ma zastosowania do bardzo wielu języków śmieci. W Javie brakuje możliwości dowiedzenia się, kiedy zabraknie określonego zasobu. Operacje typu i nawiasowe są przydatne i radzą sobie z błędami, ale nie jestem nimi zainteresowany. Chcę po prostu przydzielić zasoby, a następnie posprzątają się, gdy jest to wygodne lub konieczne, i nie ma sposobu, aby to zepsuć. Chyba nikt tak naprawdę nie przyjrzał się temu.
czytnik myśli
2

Cała pamięć jest równa, jeśli poproszę o 1K, nie obchodzi mnie, skąd w przestrzeni adresowej pochodzi 1K.

Kiedy pytam o uchwyt pliku, chcę uchwyt do pliku, który chcę otworzyć. Otwarcie uchwytu pliku na pliku często blokuje dostęp do pliku innym procesom lub maszynie.

Dlatego uchwyty plików muszą zostać zamknięte, gdy tylko nie będą potrzebne, w przeciwnym razie blokują dostęp do pliku, ale pamięć musi zostać odzyskana dopiero po jego wyczerpaniu.

Uruchomienie przepustki GC jest kosztowne i odbywa się tylko „w razie potrzeby”, nie można przewidzieć, kiedy inny proces będzie potrzebował uchwytu pliku, którego proces może nie używać, ale nadal jest otwarty.

Ian Ringrose
źródło
Twoja odpowiedź uderza w prawdziwy klucz: pamięć jest zamienna, a większość systemów ma wystarczająco dużo, aby nie trzeba jej było odzyskiwać szczególnie szybko. Natomiast jeśli program uzyska wyłączny dostęp do pliku, który zablokuje wszelkie inne programy we wszechświecie, które mogą potrzebować tego pliku, bez względu na to, ile innych plików może istnieć.
supercat
0

Sądzę, że powodem, dla którego nie podchodzono do tego zbyt często w przypadku innych zasobów, jest właśnie to, że większość innych zasobów woli być zwolniona jak najszybciej, aby ktokolwiek mógł ich ponownie użyć.

Pamiętaj oczywiście, że twój przykład można teraz podać za pomocą „słabych” deskryptorów plików przy użyciu istniejących technik GC.

Mark Hurd
źródło
0

Sprawdzenie, czy pamięć nie jest już dostępna (a tym samym zagwarantowane, że nie będzie już używana) jest raczej łatwe. Większość innych rodzajów zasobów można obsłużyć mniej więcej tymi samymi technikami (tj. Pozyskiwanie zasobów to inicjalizacja, RAII i jego odpowiednik zwalniania, gdy użytkownik zostanie zniszczony, co łączy go z administracją pamięcią). Wykonanie pewnego rodzaju uwolnienia „na czas” jest ogólnie niemożliwe (sprawdź problem zatrzymania, musisz dowiedzieć się, że jakiś zasób był używany po raz ostatni). Tak, czasami można to zrobić automatycznie, ale jest to znacznie bardziej bałagan w przypadku pamięci. Dlatego w większości zależy od interwencji użytkownika.

vonbrand
źródło