Garbage Collector, cz. III (zasoby niezarządzane)  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2013-05-29

Wprowadzenie

Poprzednie części artykułu dotyczyły zasady działania Garbage Collector. Z kolei, trzecia część będzie dotyczyć klas dostarczonych przez .NET Framework, które mają za zadanie ułatwić pracę z GC oraz jego optymalizację. Jak już to zostało wiele razy wspomniane, to nieprawda, że programista nie musi martwić się o zarządzanie pamięcią w C#.

Pisanie solidnego kodu – Constrained Execution Regions (CERs)

Co prawda, CER bezpośrednio nie ma nic wspólnego z Garbage Collector, ale prawidłowe zrozumienie tego mechanizmu jest niezbędne, aby świadomie korzystać z niezarządzanych zasobów oraz klas opisanych w dalszej części artykułu.

Co w praktyce oznacza pisanie solidnego kodu? 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 naprawdę wiele. Czasami jednak jest to konieczne, głównie w aplikacjach serwerowych, które muszą działać, nawet gdy dostarczone dane są np. nieprawidłowe. W przypadku awarii, niedopuszczalne jest wówczas 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 w nim zawarty zostanie wykonany nawet w sytuacji, w której wyrzucany jest wyjątek ThreadAbortException. Dlaczego słowo „gwarantuje” zostało umieszczone w cudzysłowie? Mogą wystąpić pewne nieoczekiwane scenariusze zdarzeń, np. komputer zostanie wyłączony w momencie wykonywania finally (awaria zasilacza), a mimo to programiści powinni obsłużyć je prawidłowo. Wywołanie jakiegokolwiek kodu powoduje wykonanie kilku czynności, m.in.:

  • załadowanie wymaganych bibliotek do pamięci,
  • wykonanie kompilacji IL do kodu natywnego,
  • wywołanie statycznych konstruktorów,
  • alokację pamięci.

KAŻDA z powyższych operacji może zakończyć się niepowodzeniem. Jako programiści chcemy zminimalizować ryzyko awarii w finally. Jeśli kod wywoła wyjątek w finally, stan aplikacji może być nieprawidłowy – jest to krytyczny fragment aplikacji. Oczywiście ciągle mowa o aplikacjach, np. serwerowych, które wymagają tak wysokiej niezawodności (reliability). W jaki sposób można zapobiec przedstawionym problemom? Jednym z rozwiązań jest CER, który wykona przedstawione operacje przed wejściem w try (wywołanie konstruktorów statycznych, JIT, alokacja pamięci, itp.). Jeżeli np. zabraknie pamięci, stanie się to przed try. Aplikacja wprawdzie zakończy działanie, ale z prawidłowym stanem, co jest niesłychanie ważne. CER ma zadanie upewnić się, czy przed wejściem w try znajduje się wystarczająco dużo zasobów na wykonanie ewentualnego „recovery”. Jeśli nie ma, wówczas lepiej wywołać wyjątek przed klauzulą try.

Działanie CER można zaprezentować na przykładzie konstruktora statycznego. Konstruktor statyczny wykonywany jest zwykle w momencie odwołania się do 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”. Konstruktor statyczny, JIT, itp., wykonywane są najczęściej dopiero, gdy zajdzie taka potrzeba. W przypadku CER 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. Przeszukiwane są tylko metody 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. Z kolei, właściwość CER 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 spowodować 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 funkcji, które potrafią rozwiązać problem z wirtualnymi metodami:

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);

Wspomniany atrybut nie spowoduje, że kompilator będzie sprawdzał, czy Consistency lub CER mają prawidłowe wartości – to, czy funkcja zmienia stan AppDomain, czy nie zależy do nas. Musimy samodzielnie określić prawidłową spójność danych.

Inną, interesującą metodą w RuntimeHelpers jest ExecuteCodeWithGuaranteedCleanup:

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

Jeśli w kodzie nie ma klauzul try-catch, można wciąż korzystać z CER, przekazując po prostu odpowiednie wskaźniki (patrz niżej) do funkcji:

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

Zasoby niezarządzane i optymalizacja GC

Garbage Collector nic nie wie o zasobach niezarządzanych. Nie wie, ile pamięci one zajmują oraz nie jest w stanie takich zasobów zwolnić. Jak już zostało wyjaśnione we wcześniejszych artykułach, GC odpalany jest zwykle po przekroczeniu pewnego progu zużycia pamięci. Niestety, skoro GC nie wie nic o niezarządzanych zasobach, to zużycie pamięci pozostanie większe niż tak naprawdę powinno być. Co w przypadku, gdy zarządzany wrapper zużywa bardzo mało pamięci, a zasoby niezarządzane w nim konsumują bardzo dużo pamięci?

Dzięki metodom AddMemoryPressure i RemoveMemoryPressure można poinformować o tym fakcie GC. Spowoduje to, że zostanie on uruchomiony wcześniej, zwalniając wspomniane wrappery:

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

Powyższe metody są wskazówkami dla GC, informującymi go o zasobach, do których on sam nie ma dostępu. 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 wrapper zajmuje zwykle mało pamięci. Z kolei, niezarządzane zasoby mogą pochłaniać jej bardzo dużo, w zależności od rozmiaru bitmapy. Jest prawdopodobne, że GC będzie posiadał bardzo dużą liczbę takich obiektów, ponieważ nie stanowią one, z jego punktu widzenia, 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 podaje się nazwę zasobu (własną) oraz ilość dozwolonych instancji, jakie powinny być trzymane w pamięci. Pewne niezarządzane zasoby mają ilość limitowaną – tzn. można stworzyć np. maksymalnie pięć uchwytów do nich. W celu stworzenia kolejnych należy najpierw zwolnić poprzednie. Dzięki HandleCollector, można 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 polegają wewnętrznie na wywołaniu metody/funkcji GC.Collect po spełnieniu podanych warunków.

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, celem MemoryFailPoint jest zminimalizowanie ryzyka wystąpienia sytuacji, w której zabraknie pamięci podczas wykonywania kodu. Klasa MemoryFailPoint 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 pamięć jest dostępna. Jeśli jej nie ma, zostanie uruchomiony GC, aby zwolnić zbędne zasoby. Jeśli wciąż nie ma wystarczającej pamięci wyrzucony zostanie wyjątek InsufficientMemoryException. Jeżeli konstruktor nie spowodował wyjątku, oznacza to, że istnieją wystarczające zasoby pamięciowe. Należy pamiętać, że MemoryFailPoint nie alokuje tak naprawdę pamięci, co oznacza, że wykonanie następnego kodu może wciąż spowodować OutOfMemoryException. MemoryFailPoint służy jedynie do zminimalizowania ryzyka i należy wziąć to pod uwagę podczas implementacji algorytmu (analogiczne zachowanie reprezentuje CER).

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

try
{
    using (MemoryFailPoint mfp = new MemoryFailPoint(5000))
    {
      // wykonanie algorytmu potrzebującego dużo pamięci
    }
}
catch (InsufficientMemoryException e)
{
     // niestety nie ma wystarczającej pamięci i nie ma, co nawet algorytmu rozpoczynać.
}

Minimalną wartością, którą można 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 np. dla 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.

Implementacja Finalize oraz CriticalFinalizerObject

O destruktorach wspomniałem już w pierwszej części artykułu. W wielkim skrócie – zawsze należy przemyśleć decyzje o implementacji finalize, ponieważ związane jest to ze 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. Klasa CriticalFinalizerObject daje kilka dodatkowych gwarancji, jeśli chodzi o wywołanie destruktora. W swej zasadzie jest bardzo podobna do CER, ale dotyczy oczywiście destruktorów.

Aby skorzystać z CriticalFinalizerObject, należy zawsze dziedziczyć po niej. Wówczas pochodna klasa zyskuje następujące właściwości:

  1. Tak samo, jak w przypadku CER, kod w finalize zostanie skompilowany dużo wcześniej. Daje to „gwarancję”, że nie wystąpi OutOfMemoryException. Pamięć alokowana jest z góry, co ma pomóc w wywołaniu finalize. Należy mieć na uwadze, że w destruktorze najprawdopodobniej zostanie umieszczony kod zwalniający zasoby niezarządzane. Jeżeli CLR nie będzie miał wystarczającej pamięci na skompilowanie i wykonanie kodu finalize, wtedy wystąpi wyciek 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 więc na zasobach niezarządzanych. Z tego względu posiada wskaźnik do kodu niezarządzanego (w formie SafeHandle, ale o tym później). SafeHandle dziedziczy po CriticalFinalizerObject, zatem jego finalizer zostanie 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 zostanie wykonany nawet wtedy, kiedy nastąpi usunięcie AppDomain z pamięci w sposób nieoczekiwany (głównie przez środowisko uruchomieniowe).

CriticalFinalizerObject ma bardzo ważne zastosowanie w klasie SafeHandle, ale o tym już wkrótce. Korzystanie z samego CriticialFinalizerObject jest bardzo proste – jak już zostało wspomniane wystarczy po nim dziedziczyć:

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

Obiekt SafeHandle

SafeHandle jest bardzo często wykorzystywany w sytuacjach, w których należy przechowywać wskaźnik do zasobów niezarządzanych. Definicja:

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

Co to oznacza? SafeHandle zawiera również wszystkie wcześniej opisane rzeczy, które oferuje CriticalFinalizerObject. Do dyspozycji jest CER, co oznacza w praktyce bezpieczny finalizer. Oczywiście klasa również implementuje IDisposable, więc można korzystać z metody Dispose lub using, co daje większą kontrolę nad zasobami. Klasa jest jednak abstrakcyjna, zatem bezpośrednio nie można jej użyć. W praktyce jednak korzysta się z innych klas, które dziedziczą po SafeHandle i implementują potrzebne metody. SafeHandle posiada kilka abstrakcyjnych elementów:

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

W ReleaseHandle należy umieścić 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. Uchwyty są nieprawidłowe wówczas, 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żeli trzeba napisać własny SafeHandle, wówczas lepiej skorzystać z powyższej klasy, ponieważ prawie zawsze implementacja Invalid będzie właśnie tak wyglądała. 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ć, do dyspozycji są implementacje, takie jak: SafeFileHandle, SafeWaitHandle czy SafeRegistryHandle. Zajrzyjmy do implementacji ReleaseHandle klasy SafeFileHandle (w tym celu można użyć np. dekompilatora .NET Reflector):

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

SafeWaitHandle wygląda dość podobnie:

[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;
}

Win32Native.CloseHandle służy do zwolnienia uchwytu. Microsoft zdecydował się na taki krok ze względu na przejrzystość API. Chciał również uniemożliwić przekazanie SafeFileHandle w sytuacji, w której metoda spodziewa się SafeWaitHandle.

Jeśli powyższe wprowadzenie teoretyczne jest niejasne, to warto przeanalizować praktyczny przykład i powrócić ponownie do opisanych mechanizmów. Załóżmy, że chcemy skorzystać z niezarządzanego mutexa, który znajduje się w kernel32.dll. W dokumentacji MSDN można znaleźć następujące informacje o sygnaturze CreateMutex:

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

Widać, że funkcja zwraca uchwyt do nowego mutexa. Proste, ale bardzo złe podejście podczas importu CreateMutex mogłoby wyglądać 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 skorzystać z SafeHandle. Lepszym rozwiązaniem jest:

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 uchwyt zostanie zwolniony, bo w końcu GC musi w pewnym momencie zebrać każdy nieosiągalny 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 najpierw uchwyt przekazany został do takiej funkcji, a potem inny wątek chce go zwolnić. W momencie, gdy SafeHandle przekazywany jest do niezarządzanej funkcji, wewnętrzny licznik jest zwiększany, co oznacza, że obiekt wciąż jest gdzieś 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. Można go też kontrolować 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.

Zakończenie

Obsługa zasobów niezarządzanych jest szczególnie trudna, ponieważ GC nic o niej nie wie. GC samodzielnie nie zwolni takich zasobów, ale może wywołać destruktor zarządzanego wrappera, który z kolei wywoła funkcję Win32, zwalniającą pamięć zadeklarowaną pod danym wskaźnikiem. Warto mieć na uwadze, że zasoby niezarządzane występują niemalże w każdej aplikacji – najpopularniejsze to bitmapy i pliki. Programiści często korzystają z gotowych wrapperów i dlatego nie zdają sobie sprawy, że tak naprawdę korzystają z niezarządzanej pamięci.

 


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.