Dwa pytania o zachowanie się obiektów od uczącego się

Dwa pytania o zachowanie się obiektów od uczącego się
AL
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 21
0

Witam serdecznie,

mam dwa pytania, na które nie mogę znaleźć odpowiedzi.

  1. Czy w NET.Core 8.0 funkcjonuje jeszcze destruktor? Czy zamiast tego używa się już tylko Using, powiązanego IDisposable i Dispose()? A jeśli funkcjonuje, to jak go wywołać? Poniższy kod uruchomiony na NET.Framework niszczy obiekt po podstawieniu do niego null i wywołuje wspomniany destruktor. Natomiast po wrzuceniu do NET.Core 8.0 już nie zachowuje się w ten sam sposób, właściwie sygnalizując nawet nie do końca właściwie przypisanie ze względu na to, że obiekt nie został zadeklarowany jako nullable.

    Kopiuj
    class Program
    {
        static void Main(string[] args)
        {
            Program program = new Program();
            program.StartTest();
        }
    
        private void StartTest()
        {
            TestObject testObject = new TestObject();
            Console.ReadKey();
            testObject = null;
        }
    }
    
    class TestObject
    {
        public TestObject() 
        {
            Console.WriteLine("I am inside Constructor");
        }
    
        ~TestObject() 
        {
            Console.WriteLine("I am inside Destructor");
        }
    }
    
    
  2. I nurtuje mnie jeszcze kwestia żywotności obiektów. Czy to jest coś, czym należy zaprzątać sobie głowę? Czy GC wystarczająco to ogarnie? I skąd wiadomo, który obiekt będzie żył własnym życiem, a który zostanie automatycznie uprzątnięty (jeśli tak się w ogóle dzieje)? Ja mam taką dziwną wizję w głowie, że każde new() utworzy kolejny obiekt, coraz bardziej zajmując pamięć, aż jej zabraknie. Poniżej taki przykład, który na szybko napisałem

W metodzie StartTest() w pierwszej linii tworzę obiekt jako instancję RandomValueWritterWithTimer, która zawiera wewnątrz timer. Po utworzeniu obiektu, timer co 2 sekundy zapisuje wartość losową do pliku txt. I to działa. Rozumiem, że taki obiekt będzie żył i nie zostanie usunięty przez GC.
W kolejnej linijce tworzony jest timer wewnątrz klasy Program, którego wywołanie tworzy nowy obiekt jako instancję klasy RandomValueWritter i wywołując metodę WriteRandomValue() również dokonuje zapisu do pliku txt, ale o innej nazwie.
I teraz pytanie - czy każdy obiekt tworzony co 2 sekundy jako instancja RandomValueWritter zostanie w pamięci? Czy zostanie automatycznie zniszczony przez GC?
Celowo użyłem using w przypadku StreamWrittera, żeby nie zostawić czasem otwartego pliku.

Kopiuj
using System.Timers;

class Program
{
    static void Main(string[] args)
    {
        Program program = new Program();
        program.StartTest();
    }

    private void StartTest()
    {
        RandomValueWritterWithTimer randomValueWritter = new RandomValueWritterWithTimer();

        System.Timers.Timer timer = new System.Timers.Timer();
        timer.Interval = 2000;
        timer.Elapsed += Timer_Elapsed;
        timer.Start();
        Console.ReadKey();  
    }

    private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
    {
        RandomValueWritter randomValueWritter = new RandomValueWritter();
        randomValueWritter.WriteRandomValue();
    }
}

class RandomValueWritter
{
    public void WriteRandomValue()
    {
        using (StreamWriter sw = new StreamWriter(".\\RandomValues.txt", true))
        {
            Random random = new Random();
            sw.WriteLine(random.Next(0, 100));
        }
    }
}

class RandomValueWritterWithTimer
{
    public RandomValueWritterWithTimer() 
    { 
        System.Timers.Timer timer = new System.Timers.Timer();
        timer.Interval = 2000;
        timer.Elapsed += Timer_Elapsed;
        timer.Start();
    }

    private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        using (StreamWriter sw = new StreamWriter(".\\RandomValuesWithTimer.txt", true))
        {
            Random random = new Random();
            sw.WriteLine(random.Next(0,100));
        }       
    }
}

I przy okazji, jeśli mógłbym prosić o jakieś wskazówki, co należałoby tutaj zmienić, dorobić, żeby napisany przeze mnie program był jak najbardziej zgodny ze sztuką.
Może gdzieś coś powinno być w innym miejscu, coś może sprawdzone wcześniej zanim obsłużone? Gdzieś może istnieje jakaś luka, która może coś spowodować w jakiejś sytuacji? Każda wskazówka dla mnie na tym etapie na wagę złota.
Wiem, że chyba Random random = new Random(); mógłbym zapisać Random random = new();

Z góry dziękuję

AD
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 50
2

Z tego co się orientuję (mogę się mylić) to destuktor jest wywoływany w momencie kiedy GC usuwa obiekt, czyli nie wiadomo dokładnie kiedy to się stanie.
Jeżeli zależy nam na natychmiastowym wykonianiu operacji po skorzystaniu w obiektu to implementujemy interfejs IDisposable i używamy go z usingiem w miejscu deklaracji obiektu.

Jeżeli chodzi o pisanie standardowego kodu to nie musisz sie tym przejmować ponieważ GC sam kasuje obiekty w pamięci kiedy nie ma do nich zadnych referencji i kiedy jest na to odpowiedni moment (np konczy sie pamiec). Wieksze obiekty kasują sie rzadziej, gdyż wymaga to wiekszej ilości czasu i może powodowac problemy z wydajnością aplikacji podczas ich usuwania.

W metodzie StartTest() tworzysz Timer i go startujesz.
Referencja do niego zniknie po zakonczniu metody StartTest(), więc wykonywanie operacji co 2 sekundy zakonczy sie w momencie kiedy GC zdecyduje go usunac z pamieci. To znaczy ze jezeli chcesz miec timer dzialajacy timer przez caly czas zycia aplikacji to musisz trzymac gdzies do niego referencje, inaczej w pewnym momencie jego kod przestanie sie wykonywac.

SL
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 1023
3

Czy w NET.Core 8.0 funkcjonuje jeszcze destruktor? Czy zamiast tego używa się już tylko Using, powiązanego IDisposable i Dispose()? A jeśli funkcjonuje, to jak go wywołać?

W językach z GC finalizer/destruktor to tylko ostatnia deska ratunku. Design GC (o czym powiem później) opiera się na tym, że samo sprzątanie robi się od czasu do czasu, więc zasoby nie związane z pamięcią (np. otwarte pliki) powinny być zwalniane tak szybko jak to możliwe

W normalnym kodzie nie powinieneś myśleć o destruktorach. To tylko gaszenie pożaru, gdzie twoim zadaniem powinno być jego prewencja

Czy GC wystarczająco to ogarnie? I skąd wiadomo, który obiekt będzie żył własnym życiem, a który zostanie automatycznie uprzątnięty (jeśli tak się w ogóle dzieje)

Praktycznie wszystkie GC (poza Pythonem, który ma ref counting jak i "normalne" GC) to Tracing GC. Co jakiś czas twoja aplikacja skanuje wszystkie możliwe początkowe punkty grafu referencji (wszystkie globale, stosy wszystkich wątków, lecące w danej chwili wyjątki). Z tak rozpoczętą listą referencji rozpoczyna się skanowanie grafu (stąd tracing), które oznacza te referencje, które żyją jak i te do których nie ma dostępu. Więc wszystko zostaje wyłapane. To co nie zostanie wyłapane to błędy po stronie kodu np. jakiś global, który trzyma informacje, których nie powinien trzymać albo wątek, który żyje a nie powinień

Źle myślisz o tym, że obiekt żyje własnym życiem. Obiekty to tylko pola i odniesienia do innych obiektów. To co naprawdę żyje własnym życiem to jakiś wątek/task odpalony przez metodę Start(). Jak powiedziałem GC zwalnia tylko pamięć. Start() odpala jakiś wątek pod spodem i to jego zwolnienie jest konieczne, jeśli nie chcesz wycieku wątku i pamięci trzymanej przez ten wątek. Taki wątek będzie entry pointem do skanowania grafu obiektów, więc śmierć wątku automatycznie sprawi, że obiekty przez niego używane nie będą "reachable", więc GC sobie poradzi

Powtórzę jeszcze raz: GC powinien zwalniać tylko pamięć. Inne zasoby (wątki, sloty, pliki, połączenia) powinny być zwalniane przez interfejs IDisposable

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
3
Almatea napisał(a):
  1. Czy w NET.Core 8.0 funkcjonuje jeszcze destruktor?

Owszem.

Czy zamiast tego używa się już tylko Using, powiązanego IDisposable i Dispose()?

Owszem, używa się.
Tylko co, jeśli ktoś zapomni? :) Destruktory nadal są potrzebne, aby zwolnić zasoby niezarządzane, które nie zostały zwolnione wcześniej, w prawidłowy sposób.

Dlatego Dispose i finalizer powinny wołać ten sam kod zwalniający zasoby niezarządzane. Więcej o tym tutaj: https://learn.microsoft.com/en-us/dotnet/api/system.idisposable?view=net-9.0

A jeśli funkcjonuje, to jak go wywołać? Poniższy kod uruchomiony na NET.Framework niszczy obiekt po podstawieniu do niego null i wywołuje wspomniany destruktor. Natomiast po wrzuceniu do NET.Core 8.0 już nie zachowuje się w ten sam sposób, właściwie sygnalizując nawet nie do końca właściwie przypisanie ze względu na to, że obiekt nie został zadeklarowany jako nullable.

Przypisanie null do zmiennej nie ma żadnego związku z działaniem GC. Zmienna nie wskazuje już na obiekt, ale obiekt wciąż jest zaalokowany w pamięci.
Destruktor jest wywoływany przez GC, więc możliwe, że wymuszenie jego działania przez np. GC.Collect() spowoduje sprzątnięcie tego obiektu. Ale nie ma na to gwarancji.

  1. I nurtuje mnie jeszcze kwestia żywotności obiektów. Czy to jest coś, czym należy zaprzątać sobie głowę? Czy GC wystarczająco to ogarnie? I skąd wiadomo, który obiekt będzie żył własnym życiem, a który zostanie automatycznie uprzątnięty (jeśli tak się w ogóle dzieje)? Ja mam taką dziwną wizję w głowie, że każde new() utworzy kolejny obiekt, coraz bardziej zajmując pamięć, aż jej zabraknie. Poniżej taki przykład, który na szybko napisałem

I tak, i nie.
Jeśli będziesz tworzył obiekty bardzo szybko, zanim GC zdąży się uruchomić, to faktycznie możesz zapchać pamięć. Więc jak najbardziej jest to coś, o czym należy myśleć. Ale nie chodzi o to, aby unikać tworzenia obiektów w ogóle, ale o to, aby nie tworzyć dużych obiektów w hurtowych ilościach, np. nie łącząc dużych stringów w pętli.

I teraz pytanie - czy każdy obiekt tworzony co 2 sekundy jako instancja RandomValueWritter zostanie w pamięci? Czy zostanie automatycznie zniszczony przez GC?

Zostanie uprzątnięty dość szybko, bo ma krótki cykl życia, i poza metodą nie ma do niego odwołań.
Ogólnie łatwo to sprawdzić, Visual Studio pokazuje zajętą przez proces pamięć w czasie.

Wiem, że chyba Random random = new Random(); mógłbym zapisać Random random = new();

Przede wszystkim nie ma sensu tworzyć obiektu tej klasy co każde użycie, lepiej trzymać go w statycznym polu.

FA
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: warszawa
  • Postów: 315
1

Przypisanie null do zmiennej nie ma żadnego związku z działaniem GC. Zmienna nie wskazuje już na obiekt, ale obiekt wciąż jest zaalokowany w pamięci.

Ma leciutki bo moze przyspieszyc usuniecie obiektu przez GC, wiek jak recznie w pisujesz nulla to taki obiekt jest gotowy do zwolnienia na najblizszej odpowiedniej sesji gc.
Ma to znaczenie w egzotycznych scenariuszach.

obscurity
  • Rejestracja: dni
  • Ostatnio: dni
2
somekind napisał(a):

Wiem, że chyba Random random = new Random(); mógłbym zapisać Random random = new();

Przede wszystkim nie ma sensu tworzyć obiektu tej klasy co każde użycie, lepiej trzymać go w statycznym polu.

wbrew pozorom użycie Random nie jest takie proste jeśli chodzi o wielowątkowość, zwłaszcza jeśli chcemy mieć ustalony seed. Dla randomowego seeda nie musimy używać żadnego pola - od .NET 6 powinno się używać Random.Shared który w przeciwieństwie do statycznej instancji Random nie stwarza problemów przy dostępie z wielu wątków i nie generuje przypadkowych zer.
Random.Shared zwraca instancję prywatnej klasy ThreadSafeRandom. Prawidłowy kod powinien wyglądać tak:

Kopiuj
sw.WriteLine(Random.Shared.Next(0, 100));

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.