Code Review: serializacja a właściwości

Załóżmy, że mamy następujący kod:

[Serializable]
class Person
{
    public string Name { get; set; }
}

Atrybut Serializable mówi, że będzie wspierana serializacja za pomocą IFormatter (np. BinaryFormatter). Jeśli chcemy wspierać ten mechanizm, zawsze powinniśmy tworzyć jawnie backing-field. Kompilator w końcu może wygenerować tak naprawdę dowolną nazwę. Zajrzyjmy do Reflector, aby zobaczyć jak wygląda wewnętrzna struktura:

 

image

Wygenerowane pole to:

[CompilerGenerated]
private string <Name>k__BackingField;

Nie ma żadnej gwarancji, że te automatycznie wygenerowane pole zawsze będzie miało taką samą nazwę. Serializacja dokonywana jest na podstawie pól a nie właściwości dlatego jest to tak ważne. Poprawna wersja to:

[Serializable]
class Person
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

Teraz nie jesteśmy skazani na implementację wewnętrzną kompilatora.

Reflection: optymalizacja MemberInfo

W celu wykonania jakiegoś kodu za pomocą refleksji, należy najpierw zebrać informację o danym polu. Załóżmy, że mamy następującą klasę:

class Sample
{
    public void PrintSomething()
    {
        Console.WriteLine("Something");
    }
}

W celu wykonania metody za pomocą refleksji można:

object sample = new Sample();
MethodInfo info=sample.GetType().GetMethod("PrintSomething");
info.Invoke(sample, null);

Generalnie obiekty takie jak MemberInfo czy MethodInfo pożerają mnóstwo pamięci – są one dość ciężkie dla CLR. Klasa w końcu może mieć bardzo wiele elementów: metody, pola, zdarzenia, konstruktory, właściwości itp. Jeśli istnieje potrzeba przechowywania tego w pamięci jest to marnotrawstwo. Zwykle nie potrzebujemy tych wszystkich informacji jakie dają nam powyższe obiekty. Jeśli naszym celem jest przechowywanie MethodInfo tylko po to aby wykonać później daną operację (a nie aby dowiedzieć się czegoś więcej o danym polu), wtedy dużo lepiej jest korzystać po prostu ze wskaźnika:

object sample = new Sample();
MethodInfo heavyObject = sample.GetType().GetMethod("PrintSomething");
RuntimeMethodHandle lightObject = heavyObject.MethodHandle;

RuntimeMethodHandle zawiera jako właściwość zwykły IntPtr – wskaźnik. Jak z tego korzystać?

object sample = new Sample();
MethodInfo heavyObject = sample.GetType().GetMethod("PrintSomething");
RuntimeMethodHandle lightObject = heavyObject.MethodHandle;
MethodBase.GetMethodFromHandle(lightObject).Invoke(sample,null);

Oczywiście musimy stworzyć MemberInfo ale robimy to tylko w momencie gdy chcemy wykonać dany element. W pamięci przechowujemy wyłącznie bardzo tanie wskaźniki.Wszystkie klasy dziedziczące po MemberInfo mają swoje “tanie” odpowiedniki tzn.:

1. Type może być skonwertowany do uchwytu za pomocą statycznej metody Type.GetTypeHandle i z powrotem do typu za pomocą Type.GetTypeFromHandle:

RuntimeTypeHandle handle=Type.GetTypeHandle(typeof (Sample));
Type type = Type.GetTypeFromHandle(handle);

 

 

2. FieldInfo posiada właściwość FieldHandle, która zwraca uchwyt oraz statyczną metodę GetFieldFromHandle, która z kolei generuje ponownie FieldInfo.

FieldInfo fieldInfo;//...
RuntimeFieldHandle handle= fieldInfo.FieldHandle;
fieldInfo=FieldInfo.GetFieldFromHandle(handle);

Zasoby niezarządzane, optymalizacja GC

GC nic nie wie o zasobach niezarządzanych. Nie wie ile pamięci one zajmują oraz oczywiście nie jest w stanie zwolnić takich zasobów. O zarządzaniu taką pamięcią pisałem już wiele razy. Opisywałem również zasadę działania GC. Zwykle jest on odpalany po przekroczeniu pewnego progu zużycia pamięci. Niestety, jak wspomniałem, GC nie wie nic o niezarządzanych zasobach. Co w przypadku gdy wrapper zużywa bardzo mało pamięci a zasoby niezarządzane w nim konsumują bardzo wiele pamięci? Dzięki metodom AddMemoryPressure i RemoveMemoryPressure, możemy powiedzieć o tym GC, tak więc wcześniej zostanie on uruchomiony zwalniając wspomniane wrappery.

public static void AddMemoryPressure(Int64 bytesAllocated);
public static void RemoveMemoryPressure(Int64 bytesAllocated);

Warto wywoływać powyższe metody, gdy chcemy dać dodatkową wskazówkę GC.  Przykład:

class BitmapWrapper
{
   private readonly Bitmap _bitmap;
   private readonly Int64 _memoryPressure;

   public BitmapWrapper(String file, Int64 size)
   {
       _bitmap = new Bitmap(file);
       if (_bitmap != null)
       {
           _memoryPressure = size;
           GC.AddMemoryPressure(_memoryPressure);
       }
   }

   public Bitmap GetBitmap()
   {
       return _bitmap;
   }

   ~BitmapWrapper()
   {
       if (_bitmap != null)
       {
           _bitmap.Dispose();
           GC.RemoveMemoryPressure(_memoryPressure);
       }
   }
}

Bitmapa jest dobrym przykładem bo zwykle wrapper zajmuje mało a zasoby niezarządzane mogą pochłaniać dużo pamięci, w zależności od rozmiaru bitmapy. Może zatem zdarzyć się, że GC będzie trzymał bardzo dużą liczbę takich obiektów, bo z jego punktu widzenia nie stanowią one obciążenia dla pamięci.

Podobną rolę pełni klasa HandleCollector:

public sealed class HandleCollector 
{

    public HandleCollector(String name, Int32 initialThreshold);
    public HandleCollector(String name, Int32 initialThreshold, Int32 maximumThreshold);
    public void Add();
    public void Remove();
    public Int32 Count { get; }
    public Int32 InitialThreshold { get; }
    public Int32 MaximumThreshold { get; }
    public String Name { get; }

}

W konstruktorze podajemy nazwę zasobu (własną) oraz ilość dozwolonych instancji, jakie powinny być trzymane w pamięci. Pewne zasoby niezarządzane mają limitowaną ilość – tzn. można stworzyć np. maksymalnie 5 uchwytów do nich. W celu stworzenia kolejnych, należy najpierw zwolnić poprzednie. Dzięki HandleCollector, możemy dać GC instrukcję, że po przekroczeniu danego limitu, GC powinien zostać uruchomiony w celu usunięcia niepotrzebnych wrapperów na niezarządzane uchwyty. HandleCollector ma wewnętrzny licznik, którym można zarządzać za pomocą Add oraz Remove. Przykład:

internal class LimitedResource
{
   private static readonly HandleCollector _handleCollector = new HandleCollector("LimitedResource", 5);

   public LimitedResource()
   {
       _handleCollector.Add();
   }
   ~LimitedResource()
   {
       _handleCollector.Remove();
   }
}

Oba rozwiązania wewnętrznie tak naprawdę wywołują GC.Collect().

MemoryFailPoint: alokowanie dużej ilości pamięci

Czasami zachodzi potrzeba wykonania krytycznego kodu, który zużywa dużo zasobów. Podobnie jak w CER, nie chcemy wykonywać kodu jeśli wiemy, że nie ma wystarczającej pamięci. W .NET istnieje klasa MemoryFailPoint, która potrafi z góry “zaalokować” określoną pamięć.

public sealed class MemoryFailPoint : CriticalFinalizerObject, IDisposable 
{
    public MemoryFailPoint(Int32 sizeInMegabytes);
    ~MemoryFailPoint();
    public void Dispose();
}

MemoryFailPoint sprawdzi czy jest dostępna pamięć. Jeśli jej nie ma, zostanie uruchomiony GC, aby zwolnić zbędne zasoby. Jeśli wciąż nie ma wystarczającej pamięci wyrzucany jest wyjątek InsufficientMemoryException.  W przypadku gdy konstruktor nie spowodował wyjątku to znaczy, że istnieją wystarczające zasoby pamięciowe. Należy pamiętać, że MemoryFailPoint nie alokuje tak naprawdę pamięci co oznacza, że wciąż wykonanie następnego kodu może spowodować OutOfMemoryException. MemoryFailPoint służy jedynie do zminimalizowania ryzyka i należy wziąć to pod uwagę podczas implementacji algorytmu.

Po wykonaniu algorytmu należy również wywołać Dispose aby zwolnić MemoryFailPoint. Dispose tak naprawdę odejmuje od pola statycznego wartość zarezerwowanej pamięci w sposób thread-safe.

try 
{
    using (MemoryFailPoint mfp = new MemoryFailPoint(5000)) 
    {
      // wykonanie algorytmu potrzebujacego duzo pamieci
    } 
}
catch (InsufficientMemoryException e) 
{
     // niestety nie ma wystarczajacej pamieci i nie ma co nawet algorytmu rozpoczynac.
}

Minimalną wartością, którą możemy przekazać do MFP jest 16. Również wszelkie wyższe wartości muszą być wielokrotnością liczby 16 lub po prostu będą odpowiednio zaokrąglone. Warto rozważyć użycie tej klasy dla np. bibliotek graficznych. Pozwolenie aplikacji na zdławienie wszelkich dostępnych zasobów nie jest dobrym pomysłem i wtedy zwykle jedynym rozsądnym wyjściem jest zakończenie procesu.

Obiekt SafeHandle

W ostatnim poście pisałem jak prawidłowo wykonać finalizację obiektu jeśli mowa o zasobach niezarządzanych, których zwolnienie jest krytyczne. Dzisiaj o obiekcie, który jest bardzo często wykorzystywany w sytuacjach gdzie należy przechowywać wskaźnik do zasobów niezarządzanych. Zacznijmy od jego definicji:

[SecurityPermissionAttribute(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable

Co to oznacza? Wszystkie rzeczy jakie daje nam CriticalFinalizerObject (patrz poprzedni post), SafeHandle również zawiera. Zatem mamy do dyspozycji CER co oznacza w praktyce bezpieczny finalizer. Oczywiście klasa również implementuje IDisposable wiec możemy korzystać z metody Dispose lub using dzięki czemu mamy większa kontrolę nad zasobami.  Klasa jest jednak abstrakcyjna więc bezpośrednio nie możemy jej użyć. W praktyce jednak korzystać będziemy z innych klas, które dziedziczą po SafeHandle i implementują potrzebne metody. Przyjrzymy się teraz abstrakcyjnym elementom SafeHandle:

public abstract class SafeHandle : CriticalFinalizerObject, IDisposable 
{
    protected abstract Boolean ReleaseHandle();
    public abstract Boolean IsInvalid{get;}
    //... (reszta kodu)
}

W ReleaseHandle umieszczamy kod, który zwalnia zasoby, z kolei IsInvalid zwraca flagę określającą czy uchwyt (wskaźnik) do zasobów niezarządzanych jest prawidłowy. Zwykle uchwyty są nieprawidłowe gdy mają wartość 0 lub –1 – tak działa WinApi. Z tego względu, .NET dostarcza dodatkową klasę SafeHandleZeroOrMinusOneIsInvalid, która przeładowuje w odpowiedni sposób IsInvalid:

public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle 
{
    //...
    // Implementaca Invalid prawdopodobnie wygląda następująco
    public override Boolean IsInvalid 
    {
        get 
        {        
            if (base.handle == IntPtr.Zero) return true;
            if (base.handle == (IntPtr) (-1)) return true;
        
            return false;
        }
    }
    //...
}

Z tego względu, jeśli musimy napisać jakiś własny SafeHandle wtedy lepiej skorzystać z powyższej klasy ponieważ prawie zawsze implementacja Invalid będzie wyglądać jak przedstawiono to w powyższym przykładzie. Oczywiście klasa jest wciąż abstrakcyjna ponieważ należy przeładować i zaimplementować ReleaseHandle. Microsoft dostarcza kilka implementacji:

System.Object
  System.Runtime.ConstrainedExecution.CriticalFinalizerObject
    System.Runtime.InteropServices.SafeHandle
      Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
        Microsoft.Win32.SafeHandles.SafeFileHandle
        Microsoft.Win32.SafeHandles.SafeMemoryMappedFileHandle
        Microsoft.Win32.SafeHandles.SafeNCryptHandle
        Microsoft.Win32.SafeHandles.SafePipeHandle
        Microsoft.Win32.SafeHandles.SafeRegistryHandle
        Microsoft.Win32.SafeHandles.SafeWaitHandle
        System.Runtime.InteropServices.SafeBuffer
        System.Security.Authentication.ExtendedProtection.ChannelBinding

Jak widać, mamy implementacje takie jak SafeFileHandle, SafeWaitHandle czy SafeRegistryHandle. Zajrzyjmy do implementacji ReleaseHandle klasy SafeFileHandle (można użyć do tego np. .NET Reflector):

[SecurityCritical]
protected override bool ReleaseHandle()
{
    return Win32Native.CloseHandle(base.handle);
}

Jak wygląda SafeWaitHandle?

[SecurityCritical]
protected override bool ReleaseHandle()
{
    if (!this.bIsMutex || Environment.HasShutdownStarted)
    {
        return Win32Native.CloseHandle(base.handle);
    }
    bool flag = false;
    bool bHandleObtained = false;
    try
    {
        if (!this.bIsReservedMutex)
        {
            Mutex.AcquireReservedMutex(ref bHandleObtained);
        }
        flag = Win32Native.CloseHandle(base.handle);
    }
    finally
    {
        if (bHandleObtained)
        {
            Mutex.ReleaseReservedMutex();
        }
    }
    return flag;
}
 

Jak widać, wygląda  dość podobnie… Win32Native.CloseHandle służy do zwolnienia uchwytu.  Microsoft zdecydował się na taki krok ze względu na przejrzystość API i aby uniemożliwić przekazanie SafeFileHandle gdy metoda spodziewa się SafeWaitHandle.

Jeśli powyższe wprowadzenie teoretyczne jest niejasne, to warto przeanalizować praktyczny przykład i wtedy powrócić ponownie do opisanych mechanizmów.  Załóżmy, że chcemy skorzystać z niezarządzanego mutex’a, który znajduje się w kernel32.dll.  Zaglądając do dokumentacji, dowiemy się o sygnaturze metody CreateMutex:

HANDLE WINAPI CreateMutex(
  _In_opt_  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  _In_      BOOL bInitialOwner,
  _In_opt_  LPCTSTR lpName
);

Widzimy, że funkcja zwraca uchwyt do nowego mutex’a.  Jeśli chcemy napisać brzydki kod, wtedy moglibyśmy zaimportować funkcję następująco:

internal class Program
{
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
   private static extern IntPtr CreateUglyMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);

   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
   public static extern bool ReleaseUglyMutex(IntPtr hMutex);

   public static void Main()
   {
       IntPtr mutex = IntPtr.Zero;

       try
       {
           mutex = CreateUglyMutex(IntPtr.Zero, true, "Nazwa");
       }
       finally
       {
           if (mutex != IntPtr.Zero)
               ReleaseUglyMutex(mutex);
       }
   }
}

Powyższa implementacja używa czystego wskaźnika (IntPtr) i z tego względu może okazać się to niebezpieczne. Co w przypadku gdy CreateUglyMutex stworzy obiekt ale nie przepisze go zmiennej mutex? Sytuacja jest jak najbardziej możliwa w przypadku ThreadAbortException. Z tego względu lepiej korzystać z SafeHandle. Jeśli takie niebezpieczeństwa nie są zrozumiałe, zachęcam do przeczytania poprzednich wpisów o CriticalFinalizerObject oraz CER. Dużo lepsze rozwiązanie to:

internal class Program
{
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
   private static extern SafeWaitHandle CreateMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);

   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
   public static extern bool ReleaseMutex(SafeWaitHandle hMutex);

   public static void Main()
   {
       SafeWaitHandle mutex = null;

       try
       {
           mutex = CreateMutex(IntPtr.Zero, true, "Nazwa");
       }
       finally
       {
           if (mutex !=null&&!mutex.IsInvalid)
               ReleaseMutex(mutex);
       }
   }
}

Kod jest dużo bardziej bezpieczny. SafeHandle implementuje IDisposable oraz destruktory (finalizers). Z tego względu, zawsze zostanie uchwyt zwolniony bo w końcu GC musi w pewnym momencie zebrać każdy niedostępny obiekt.

Ponadto, SafeHandle posiada pewne zabezpieczenie gdy przekazuje uchwyt do niezarządzanej funkcji, o której GC nic w końcu nie wie. Może okazać się , że przekazujemy uchwyt do takiej funkcji a potem jakiś inny wątek chce zwolnić uchwyt. W momencie gdy SafeHandle jest przekazywany do niezarządzanej funkcji, wewnętrzny licznik jest zwiększany, co oznacza, że obiekt jest gdzieś wciąż używany i nie może zostać usunięty z pamięci. Gdy funkcja kończy swoje działanie, licznik jest z powrotem zmniejszany o jeden. Licznik można kontrolować również samemu poprzez metody DangerousAddRef, DangerousRelease oraz DangerousGetHandle. Dostępna jest również klasa CriticalHandle, która nie implementuje przedstawionego mechanizmu z wewnętrznym licznikiem. Skutkuje to większą wydajnością ale mniejszym bezpieczeństwem. Microsoft sugeruje aby w pierwszej kolejności używać SafeHandle z licznikiem.

Array.Sort a stabilność sortowania

Funkcja Array.Sort sortuje tablicę elementów. Niestety algorytm jest niestabilny co nie zawsze jest dobrym rozwiązaniem. Rozważmy poniższy kod:

public class Person
{        
   public int Age { get; set; }
   public string Name { get; set; }
}

internal class Program
{
   public static void Main()
   {

       var persons = new[] {
       new Person { Age = 1, Name = "a" },
       new Person { Age = 2, Name = "b" },
       new Person { Age = 3, Name = "c" },
       new Person { Age = 2, Name = "e" },
       new Person { Age = 1, Name = "f" },
       new Person { Age = 4, Name = "g" },
       new Person { Age = 1, Name = "h" },
   };
       
       Array.Sort(persons, (p1, p2) => p1.Age.CompareTo(p2.Age));
       foreach (var person in persons)
       {
           Console.WriteLine("{0}:{1}", person.Name, person.Age);
       }
   }
}

W stabilnych algorytmach jeśli mamy sekwencję (3,’a’), (5,’b’), (3,’c’) i posortujemy ją po pierwszym kluczu to na wyjściu zawsze będzie (3,’a’),(3,’c’), (5,’b’). W niestabilnych może okazać się, że dostaniemy (3,’c’), (3,’a’),  (5,’b’). Matematycznie jest to jak najbardziej poprawne ale już w interfejsach użytkownika nie zawsze jest to pożądany efekt. Innymi słowy, kolejność kluczy nie jest zachowywana. Jednym z rozwiązań jest użycie LINQ i funkcji OrderBy, które zawsze wykonują stabilne sortowanie. Jeśli jednak upieramy się przy Array.Sort możemy użyć własnego Comparer’a (źródło forum StackOverFlow):

public class Person
{
   public int Age { get; set; }
   public string Name { get; set; }
}
public static class ArrayExtensions
{
   public static void StableSort<T>(this T[] values, Comparison<T> comparison)
   {
       var keys = new KeyValuePair<int, T>[values.Length];
       for (var i = 0; i < values.Length; i++)
           keys[i] = new KeyValuePair<int, T>(i, values[i]);
       Array.Sort(keys, values, new StabilizingComparer<T>(comparison));
   }

   private sealed class StabilizingComparer<T> : IComparer<KeyValuePair<int, T>>
   {
       private readonly Comparison<T> _comparison;

       public StabilizingComparer(Comparison<T> comparison)
       {
           _comparison = comparison;
       }

       public int Compare(KeyValuePair<int, T> x,
                          KeyValuePair<int, T> y)
       {
           var result = _comparison(x.Value, y.Value);
           return result != 0 ? result : x.Key.CompareTo(y.Key);
       }
   }
}
internal class Program
{
   public static void Main()
   {

       var persons = new[]
                         {
                             new Person {Age = 1, Name = "a"},
                             new Person {Age = 2, Name = "b"},
                             new Person {Age = 3, Name = "c"},
                             new Person {Age = 2, Name = "e"},
                             new Person {Age = 1, Name = "f"},
                             new Person {Age = 4, Name = "g"},
                             new Person {Age = 1, Name = "h"},
                         };

       persons.StableSort((p1, p2) => p1.Age.CompareTo(p2.Age));
       foreach (var person in persons)
       {
           Console.WriteLine("{0}:{1}", person.Name, person.Age);
       }
   }
}

Metoda Array.Sort przyjmuje również tablicę kluczy. Dzięki niej jesteśmy w stanie zachować kolejność, gdy dwa klucze są takie same. Oczywiście gdy wszystkie wartości są różne od siebie wtedy nie ma problemu ze stabilnością, ponieważ kolejność może być tylko jedna…

Implementacja Finalize oraz CriticalFinalizerObject

O destruktorach pisałem już kilka razy na blogu. W wielkim skrócie – zawsze należy przemyśleć decyzje o implementacji Finalize ponieważ wiąże to się z spadkiem wydajności (obiekt może być nawet “wypromowany” do drugiej generacji GC). Czasami jednak zachodzi taka potrzeba – głównie w przypadku użycia niezarządzanych zasobów. CriticalFinalizerObject daje nam jeszcze kilka dodatkowych gwarancji. Przed przeczytaniem tego wpisu, polecam zapoznanie się z poprzednim postem o CER.

Co zatem da nam dziedziczenie po CriticalFinalizerObject? Po kolei:

  1. Tak samo jak w przypadku CER, kod w Finalize będzie skompilowany dużo wcześniej. Daje to gwarancje, że nie wystąpi OutOfMemoryException. Pamięć jest alokowana z góry co ma pomóc w zagwarantowaniu wywołania FInalize. Pamiętajmy, że w destruktorze najprawdopodobniej umieścimy kod zwalniający zasoby niezarządzane. W przypadku gdyby CLR nie miał wystarczającej pamięci na skompilowanie i wykonanie kodu Finalize, wtedy będziemy mieć do czynienia z wyciekiem pamięci.
  2. Wszystkie destruktory obiektów, które dziedziczą po CriticalFinalizerObject, będą wywołane PO wykonaniu Finalize w obiektach niedziedziczących po CriticalFinalizerObject. Dlaczego jest to takie ważne? Dobrym przykładem jest klasa FileStream. Umożliwia ona operacje na plikach a zatem na zasobach niezarządzanych. Z tego względu posiada wskaźnik do kodu niezarządzanego (w formie SafeHandle ale o tym w następnym w wpisie).  SafeHandle dziedziczy po CriticalFinalizerObject zatem jego finalizer będzie wykonany przed destruktorem klasy FileStream, która nie dziedziczy po CrtiticalFinalizerObject. W momencie zwalniania zasobów, FileStream może dokonać operacji flush na SafeHandle, ponieważ ma pewność, że SafeHandle nie został jeszcze zwolniony. Jest to konieczne, ponieważ FileStream buforuje dane i nie wykonuje operacji bezpośrednio na plikach(ze względu na wydajność).
  3. Finalizer będzie wykonany nawet gdy AppDomain zostanie usunięta z pamięci w sposób nieoczekiwany (głównie przez środowisko uruchomieniowe).

CriticalFinalizerObject ma bardzo ważne zastosowanie w klasie SafeHandle, ale o tym w następnym w poście. W dzisiejszym wpisie chciałem tylko przybliżyć zasadę działania CriticalFinalizerObject. Korzystanie z niego jest bardzo proste, wystarczy dziedziczyć po nim tzn.:

class CerExample:CriticalFinalizerObject
{        
   ~CerExample()
   {
       Console.WriteLine("~ctor");
   }
}

Pisanie solidnego kodu: Constrained Execution Regions (CERs)

Z pewnością każdy z Was odpowiedziałby, że pisze solidny kod. Oczywiście zależy to od przyjętych metryk i definicji “solidny kod”. Nie zawsze warto skupiać uwagę na drobiazgach  i pułapach, których jest na prawdę wiele. Czasami jednak jest to konieczność, głównie w aplikacjach serwerowych, które muszą działać, nawet, gdy dostarczone dane są np. nieprawidłowe. W przypadku awarii, niedopuszczalne jest wtedy zepsucie stanu aplikacji. Rozważmy, taką sytuację:

try
{
  // jakaś logika
}
catch(IOException e)
{
  // obsługa błędu
}
finally
{
  // rollback - przywrócenie stanu, wycofanie zmian
}

Jakie pułapki kryje powyższy kod ? Dobrą praktyką jest używanie finally. Klauzula finally  “gwarantuje”, że kod zawarty w nim, zostanie wykonany nawet w sytuacji w której jest wyrzucony wyjątek ThreadAbortException, powodujący usunięcie AppDomain (bardzo ważne w aplikacjach serwerowych). Dlaczego słowo gwarantuje umieściłem w cudzysłowie? Oczywiście możemy mieć po prostu pecha, i komputer zostanie wyłączony w momencie wykonywania finally (np. awaria zasilacza.). Są jednak pewne przypadki, które mogą zdarzyć się a my jesteśmy w stanie im zapobiec. Wykonanie jakiegokolwiek kodu powoduje wykonanie kilku czynności m.in.:

  1. Załadowanie wymaganych bibliotek do pamięci.
  2. Wykonanie kompilacji IL do kodu natywnego.
  3. Wywołanie statycznych konstruktorów.
  4. Alokacja pamięci.

KAŻDA z powyższych operacji, może zakończyć się niepowodzeniem. Jako programiści, chcemy zminimalizować ryzyko awarii w finally. Jest to krytyczny fragment aplikacji – jeśli kod wywoła wyjątek w finally, stan aplikacji może być nieprawidłowy. Oczywiście ciągle, mowa o aplikacjach np. serwerowych, które wymagają tak wysokiej niezawodności (reliability). Jak możemy zapobiec przedstawionym problemom? Jednym z rozwiązań jest CER, który w skrócie, wykona przedstawione operacje przed wejściem w try (wywołanie konstruktorów statycznych, JIT, alokacja pamięci itp.). Jeśli np. ma zabraknąć pamięci, stanie się to przed try a zatem aplikacja co prawda zakończy działanie ale z prawidłowym stanem, co jest niesłychanie ważne. Chcemy zatem przed wejściem w try, upewnić się, że mamy wystarczające zasoby na wykonanie ewentualnego recovery.

Działanie CER łatwo pokazać na przykładzie konstruktora statycznego. Zwykle, konstruktor statyczny jest wykonywany w momencie odwołania się do jakiś statycznych zasobów klasy:

class CerExample
{
   static CerExample()
   {
       Console.WriteLine("Type ctor");
   }      
   public static void AnyMethod(){}
}
internal class Program
{
   public static void Main()
   {
       try
       {
           Console.WriteLine("Trying...");
       }
       finally
       {
           CerExample.AnyMethod();
       }
   }
}

Typowy output dla powyższego kodu to najpierw “Trying” a potem “Type Ctor”. Najczęściej konstruktor statyczny, JIT itp. są wykonywane dopiero gdy zajdzie taka potrzeba. W przypadku CER oczywiście wszystkie operacje zostaną wykonane przed try:

class CerExample
{
   static CerExample()
   {
       Console.WriteLine("Type ctor");
   }
   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
   public static void AnyMethod(){}
}
internal class Program
{                
   public static void Main()
   {
       RuntimeHelpers.PrepareConstrainedRegions();        
       try
       {
           Console.WriteLine("Trying...");
       }
       finally
       {
           CerExample.AnyMethod();
       }
   }
}

Przed try należy wykonać metodę PrepareConstrainedRegions, która przeszuka finally i wykona operacje takie jak JIT czy alokacja pamięci już przed try. Nie są przeszukiwane wszystkie metody, ale tylko te oznaczone atrybutem ReliabilityContract. Właściwość  Consistency  atrybutu ReliabilityContract przyjmuje następujące wartości: MayCorruptProcess, MayCorruptAppDomain, MayCorruptInstance, WillNotCorruptState. Akceptowalne wartości dla CER to WillNotCorruptState albo MayCorruptInstance. Po prostu CER nie może zagwarantować poprawności metod, które mogą popsuć stan AppDomain lub całego procesu. Właściwość Cer z kolei zawiera wartości Cer.Success, MayFail albo None. Cer.None oznacza, że mechanizm Cer nie będzie wykorzystywany (domyślna wartość). Cer.Sucess oraz Cer.MayFail dokumentują, czy funkcja może spodobać błąd czy nie.

Oczywiście PrepareConstrainedRegions ma ograniczone możliwości. Niemożliwe jest np. przeanalizowanie wirtualnych metod, reflection czy zdarzeń. RuntimeHelpers zawiera jednak kilka innych metod, które potrafią rozwiązać problem z wirtualnymi metodami – ale o tym innym razem.

public static void PrepareMethod(RuntimeMethodHandle method)
public static void PrepareMethod(RuntimeMethodHandle method,RuntimeTypeHandle[] instantiation)
public static void PrepareDelegate(Delegate d);
public static void PrepareContractedDelegate(Delegate d);

Naturalnie wspomniany atrybut nie powoduje, że kompilator będzie sprawdzał czy Consistency lub Cer mają prawidłowe wartości – to czy funkcja zmienia stan AppDomain czy nie należy do naszej odpowiedzialności. Musimy zatem sami określić prawidłową spójność danych. Inną, intersującą metodą w RuntimeHelpers jest ExecuteCodeWithGuaranteedCleanup:

public static void ExecuteCodeWithGuaranteedCleanup(
    RuntimeHelpers.TryCode code,
    RuntimeHelpers.CleanupCode backoutCode,
    Object userData
)

Przyjmuje ona po prostu kod Try oraz Finally. Jeśli nie mamy w kodzie try-catch-finally, możemy wywołać powyższą metodę, przekazując wskaźniki do naszych funkcji.

public delegate void TryCode(Object userData);
public delegate void CleanupCode(Object userData, Boolean exceptionThrown);

Code Review: Parsowanie daty

Zadanie jest następujące. Mamy w pliku tekstowym dane zawierające m.in datę  z góry w zdefiniowanym formacie a mianowicie 2012/10/05 (piąty październik 2012). Dla uproszczenia, ograniczymy się tylko do dnia, miesiąca i roku, bez czasu. Pierwsze podejście, najgorsze mogłoby wyglądać następująco:

string timestamp = "2012/10/05";
DateTime dateTime = DateTime.Parse(timestamp);
Console.WriteLine(dateTime);

Dlaczego jest to niepoprawne? W niektórych ustawieniach regionalnych (np. USA) format jest następujący “yyyy/dd/MM”  tzn. najpierw jest dzień a potem miesiąc. W PL z kolei jest to zawsze posortowane czyli rok, miesiąc, dzień albo dzień, miesiąc, rok. Jeśli mamy z góry zdefiniowany format, lepiej użyć funkcji ParseExact, która akceptuje jawny format daty:

string timestamp = "2012/10/05";
DateTime dateTime = DateTime.ParseExact(timestamp,"yyyy/MM/dd",null);
Console.WriteLine(dateTime);

W UK powyższy kod zadziała. W Niemczech czy w Polsce już niekoniecznie. Dlaczego? W końcu jawnie określamy format daty i aktualne ustawienia regionalne nie powinny być brane pod uwagę. Niestety tak nie jest ponieważ slash (/) jest znakiem specjalnym tutaj i oznacza po prostu separator zależny od kultury. W Polsce, używa się myślnika(-) i data jest w formacie yyyy-MM-dd.  W jaki sposób zatem prawidłowo parsować datę? Dobrym podejściem jest przekazywanie InvariantCulture tzn.:

string timestamp = "2012/10/05";
DateTime dateTime = DateTime.ParseExact(timestamp,"yyyy/MM/dd",CultureInfo.InvariantCulture);
Console.WriteLine(dateTime);

W takim przypadku mamy pewność, że wszelkie znaki specjalne (takie jak ‘/’) będą traktowane w sposób jednakowy, na wszystkich maszynach, niezależnie od aktualnych ustawień regionalnych. Tak się składa, że InvariantCulture używa jako separatora ‘/’ więc wszystko zgadza się. Innym sposobem jest użycie cudzysłowu aby potraktować ‘/’ jako zwykły znak:

string timestamp = "2012/10/05";
DateTime dateTime = DateTime.ParseExact(timestamp,"yyyy'/'MM'/'dd",null);
Console.WriteLine(dateTime);

Warto zwrócić uwagę na to ponieważ łatwo popełnić błąd jeśli nie wie się, że slash to taki sam znak specjalny jak yyyy czy MM.