C#–zablokowane destruktory

O finalizers czy też destruktorach pisałem już wielokrotnie. Wspomniałem również, że lepiej ich unikać, jeśli oczywiście to możliwe. Głównym problem jest możliwość wypromowania obiektu do kolejnych generacji, ponieważ obiekty z destruktorami są umieszczane w kolejce freachable, gdzie na nową stają się osiągalne.

Jeśli ktoś nie pamięta, to polecam najpierw poczytanie o tym (w dziale articles są linki do moich tekstów o GC).

Dzisiaj jednak chciałbym wspomnieć o czymś innym. Wiemy, że jeden wątek zwykle monitoruje freachable i wywołuje destruktory. Z tego względu, realne zagrożenie to blokada przez jeden obiekt,  uniemożliwiająca zwolnienie zasobów wszystkich pozostałych obiektów. W każdej aplikacji jest to problem i ryzyko. Oczywiście, jeśli tworzymy jakiś rozszerzalny Engine to sprawa komplikuje się jeszcze bardziej. Zacznijmy od przykładu:

private class UglyObject
{
  private readonly string _text;
  private readonly int _time;

  public UglyObject(string text,int time)
  {
      _text = text;
      _time = time;
  }

  ~UglyObject()
  {
      Thread.Sleep(_time);
      Console.WriteLine("{0}: zwalnianie pamieci. {1} {2}",DateTime.Now,_text,_time);
  }
}

Prosta klasa z Finalizer. Następnie wywołajmy w pętli kilka inicjalizacji klasy UglyObject. Przekazujemy najpierw wartość zero do Thread.Sleep:

private static void Main(string[] args)
{
  Test();
  GC.Collect();
  GC.WaitForPendingFinalizers();
  Console.WriteLine("Koniec");
}

private static void Test()
{
  for (int i = 0; i < 10; i++)
      new UglyObject(i.ToString(CultureInfo.InvariantCulture),0);
}

Tutaj nic złego nie dzieje się. Na ekranie, po wymuszeniu GC dostaniemy oczywiście kolejne wywołania destruktora:

image

Następnie zmodyfikujmy metodę Test, tak aby Thread.Sleep blokował część wątków przez kilka sekund:

private static void Test()
{
  for (int i = 0; i < 10; i++)
  {
    new UglyObject(i.ToString(CultureInfo.InvariantCulture), i<5?60*1000:0);
  }
}

Jeśli kilka różnych wątków przetwarzałoby kolejkę freachable to możliwe byłoby natychmiastowe zwolnienie 5 ostatnich wątków, ponieważ przekazujemy tam wartość zero jako czas.

image

Jeszcze gorszy scenariusz to taki, w którym mamy jakąś synchronizacje (semafory, muteksy itp.), która może permanentnie zablokować kolejkę. Zwykle piszemy destruktory, gdy korzystamy z niezarządzanych zasobów. Niesie to ze sobą ryzyko, że kod, którego nie znamy jest wywoływany. Metody Finalizers zawsze powinny być szybkie i zwracać wynik natychmiast. Jeszcze groźniejszym przypadkiem są obiekty COM. Obiekty STA, mogą zostać wywołane wyłącznie przez wątek, które je stworzył. Jeśli taki wątek aktualnie jest zajęty, to całość będzie blokowana. Wiąże to się z Thread Affinity czyli przynależnością obiektu do wątku.

Skutek zablokowanych finalizerow to oczywiście memory leak i ciągłe zwiększanie zużycia pamięci.

W przypadku, gdy proces jest zamykany, również destruktory są wywoływane. Oczywiście CLR nie może sobie pozwolić, że jeden obiekt blokuje cały proces. Zwykle, po 2 sekundach wszystkie wątki zostaną po prostu anulowane i proces zostanie zabity.

Leave a Reply

Your email address will not be published.