Code Review: Garbage Collector a zmienne lokalne w metodach

Co wyświetli poniższy fragment kodu?

internal class Program
{
   public static void Main()
   {            
       var timer = new Timer(TimerCallback, null, 0, 1000);            
       Console.ReadLine();
   }
   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
   }
}

Powyższy konstruktor uruchamia timer i spodziewalibyśmy się, że na ekranie po prostu będą wyświetlane kolejne callbacki. W praktyce jednak dokonywana jest pewna optymalizacja, która ma fatalne efekty. Można zauważyć, że zmienna timer nie jest nigdzie tak naprawdę wykorzystywana. Z tego względu, powyższy kod można zapisać również jako (po optymalizacji w release):

internal class Program
{
   public static void Main()
   {            
       new Timer(TimerCallback, null, 0, 1000);            
       Console.ReadLine();
   }
   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
   }
}

Jakie to przyniesie skutki? GC może zwolnić pamięć przeznaczoną na timer ponieważ nie ma do niej referencji. W wyniku czego, TimerCallback nie będzie wywoływany. Można to zaobserwować uruchamiając poniższy kod w release:

internal class Program
{
   public static void Main()
   {            
       var timer = new Timer(TimerCallback, null, 0, 1000);            
       Console.ReadLine();
   }
   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
       GC.Collect();
   }
}

W trybie debug co sekundę będzie wyświetlany komunikat. W trybie release, ze względu na optymalizacje opisane wyżej, zostanie wyświetlony tylko raz TimerCallback. Oczywiście jeśli mamy pecha może ogóle nie zostać wyświetlony. W jaki sposób napisać powyższy kod, który działa dobrze nawet w trybie release?

Można oczywiście zadeklarować zmienną jako pole klasy:

internal class Program
{
   private static Timer _timer;

   public static void Main()
   {
       _timer = new Timer(TimerCallback, null, 0, 1000);
       Console.ReadLine();
   }

   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
       GC.Collect();
   }
}

Dużo lepiej w tym przypadku jest jednak wywołanie dispose po ReadLine:

internal class Program
{
   public static void Main()
   {            
       var timer = new Timer(TimerCallback, null, 0, 1000);            
       Console.ReadLine();
       timer.Dispose();
   }
   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
       GC.Collect();
   }
}

W tym przypadku nie zostanie dokonana optymalizacja ponieważ zmienna jest wykorzystywana. Analogiczne rozwiązanie to:

internal class Program
{
   public static void Main()
   {            
       using(new Timer(TimerCallback, null, 0, 1000))
       {
           Console.ReadLine();
       }
   }
   private static void TimerCallback(Object o)
   {
       Console.WriteLine("Callback: " + DateTime.Now);
       GC.Collect();
   }
}

GC może zwolnić obiekt, nawet jeśli wykonuje jego metodę. Trzeba zdać sobie sprawę, że obiekt może zostać “zebrany” w momencie gdy jest niepotrzebny. GC i JIT współpracują ze sobą i w trakcie wykonywania metody, w dowolnym momencie może zostać zmieniony stos, usuwając jakąś nieużywaną referencje. Na przykład:

class SampleClass
{
    ~SampleClass()
   {
       Console.WriteLine("Obiekt jest usuwany.");
   }
   public void Print()
   {
       GC.Collect();
       GC.WaitForFullGCComplete();
       GC.WaitForPendingFinalizers();
       Console.WriteLine("Test");
   }
}
internal class Program
{
   public static void Main()
   {
       SampleClass sampleClass=new SampleClass();            
       sampleClass.Print();
   }  
}

Spodziewalibyśmy się na wyjściu najpierw “Test” a potem dopiero “Obiekt jest usuwany”. Proszę jednak zauważyć, że wskaźnik this w metodzie Print nie jest nigdzie używany i już przed linią Console.WriteLine(“test”) może SampleClass zostać zwolniony. Ostatni moment kiedy jest wymagany dostęp do SampleClass jest w momencie wywołania metody Print. Jednakże gdyby Print używał this w Console.WriteLine wtedy na wyjściu pokaże się najpierw “Test” a potem dopiero “Obiekt jest usuwany”:

class SampleClass
{
    ~SampleClass()
   {
       Console.WriteLine("Obiekt jest usuwany.");
   }
   public void Print()
   {
       GC.Collect();
       GC.WaitForFullGCComplete();
       GC.WaitForPendingFinalizers();
       Console.WriteLine("Test {0}",GetType());
   }
}
internal class Program
{
   public static void Main()
   {
       SampleClass sampleClass=new SampleClass();            
       sampleClass.Print();
   }  
}

Zaskakujące! Może wydawać się to dziwne ale jeszcze raz powtórzę: stos może być zmieniany w każdym momencie dzięki dokonywanym optymalizacjom. Jeśli tylko dany obiekt jest nieużywany, może on zostać wtedy usunięty nawet gdy wywoływana jest na nim w danym momencie metoda!

8 thoughts on “Code Review: Garbage Collector a zmienne lokalne w metodach”

  1. Bardziej uniwersalnym rozwiązaniem od Dispose jest wykorzystanie metody GC.KeepAlive, bo można ją zastosować zawsze, nawet wtedy gdy lokalny obiekt nie implementuje interfejsu IDisposable. Natomiast to co jest IDisposable lepiej zapakować w “using”. Zapewni to żywotność obiektu w zakresie wewnętrznych “klamerek”.

  2. Dodam tylko, że artykuł bardzo trafnie wyjaśnia, że obiekty lokalne (podobnie jak wszystkie typy referencyjne) mogą być zwolnione szybciej niż się spodziewamy.

  3. @Pawel: co do using to umieściłem to w ostatnim przykładzie:)
    A co do KeepAlive to dzięki za sugestię. Może nawet o tym osobny post napiszę.

  4. Hmm, przejrzałem ten artykuł jeszcze raz i zaktualizowałem post o jeszcze jedną ciekawą próbkę. Skonstruowałem coś takiego:
    class SampleClass
    {
    ~SampleClass()
    {
    Console.WriteLine(“Obiekt jest usuwany.”);
    }
    public void Print()
    {
    GC.Collect();
    GC.WaitForFullGCComplete();
    GC.WaitForPendingFinalizers();
    Console.WriteLine(“Test”);
    }
    }
    internal class Program
    {
    public static void Main()
    {
    SampleClass sampleClass=new SampleClass();
    sampleClass.Print();
    }
    }

    Więcej info w poście oczywiście.

Leave a Reply

Your email address will not be published.