Powiadomienia o kolekcjach GC

Na wstępnie od razu chciałbym dodać, że dla większości aplikacji, dalsze rozważania w tym poście nie mają sensu i należy to traktować jako ciekawostkę. Dla części systemów może to jednak być ważne a mianowicie dla oprogramowania działającego w “czasie rzeczywistym”, wymagającego low-latency.

Dzisiejszy wpis może powstał trochę zbyt wcześnie ale wkrótce powinna pojawić się druga część mojego artykułu o GC, który będzie dotyczył różnych trybów kolekcji. Jednym z tych trybów jest wykonywanie kolekcji w sposób nierównoległy. W skrócie, znajdywanie wszystkich nieosiągalnych obiektów ma miejsce na wątku głównym (główny to ten, który powoduje kolekcje). W podejściach równoległych, istnieje osobny wątek, który na bieżąco sprawdza czy jakieś obiekty są już nieosiągalne. Podejście równoległe daje wrażenie płynności aplikacji (pauzy na kolekcje są dużo krótsze) ale wymaga to więcej zasobów i jest to na końcu po prostu trochę wolniejsze (synchronizacja itp.). Najszybsze jest po prostu zablokowanie wątków i przejście przez drzewo obiektów. Niestety powoduje to opóźnienia i w niektórych aplikacjach jest to bardzo niekorzystne – wyobraźmy sobie aplikację, wyświetlającą animację, która musi się co jakiś czas zatrzymać aby wykonać kolekcję.

Istnieje wiele rozwiązań na ten problem. Cześć z nich pokażę w nadchodzącym artykule. W dzisiejszym wpisie chciałbym zaprezentować jednak trochę inny mechanizm. Wyobraźmy sobie, że mamy rozproszony system przetwarzający tysiące zapytań. Dodajmy również kolejne wymaganie a mianowicie “low latency” – nie możemy sobie pozwolić, że jakieś zapytanie będzie opóźnione z powodu GC. W poście chciałbym pokazać rozwiązanie polegające na transferze zapytań do np. innego węzła w momencie, gdy zbliża się czas kolekcji. Jeśli zatem wiemy, że za 5 sekund będziemy mieli pełną kolekcję wtedy przekierowujemy ruch do innego węzła.

W .NET istnieją 4 funkcję, które będą nam szczególnie przydatne:

  • GC.RegisterForFullGCNotification()
  • GC.CancelFullGCNotification()
  • GC.WaitForFullGCApproach()
  • GC.WaitForFullGCComplete()

Utworzymy więc osobny wątek, który będzie monitorował stan GC. W momencie, gdy zbliża się kolekcja, wyśle on komunikat, że system będzie przez jakiś czas obciążony. W celu otrzymania powiadomień od GC należy wywołać funkcję GC.RegisterForFullGCNotification:

GC.RegisterForFullGCNotification(maxGenerationThreshold: 10, largeObjectHeapThreshold: 10);

Funkcja jest bardzo prosta ale niestety parametry wejściowe to trochę czarna magia. W dokumentacji możemy znaleźć następujący opis:

maxGenerationThreshold

Type: System.Int32

A number between 1 and 99 that specifies when the notification should be raised based on the objects surviving in generation 2.

largeObjectHeapThreshold
Type: System.Int32

A number between 1 and 99 that specifies when the notification should be raised based on objects allocated in the large object heap.

Niestety nie jest to zbyt precyzyjne a nawet powiedziałbym bardzo mylące! Podane progi to nie wartości absolutne. Jeśli przekażemy 10 i 10  nie znaczy to, że dostaniemy powiadomienie gdy 10 obiektów zostało wypromowanych do GEN2. Należy te parametry traktować jako wartości relatywne i:

  1. Podanie zbyt wysokiej liczby spowoduje, że dostaniemy powiadomienie bardzo szybko, możliwe, że długo przed faktyczną kolekcją.
  2. Podanie zbyt małej wartości może nieść ryzyko, że powiadomienia po prostu nie zostaną wysłane.

Zatem należy na nie patrzyć relatywnie – 90 wyśle powiadomienie dużo szybciej niż np. 5.

Mając mechanizm powiadomień możemy stworzyć nowy wątek, który będzie sprawdzał stan pamięci:

private static void MonitorMemory()
{
  while (true)
  {
      GCNotificationStatus gcStartedStatus = GC.WaitForFullGCApproach(-1);
      if (gcStartedStatus == GCNotificationStatus.Succeeded)                    
          Console.WriteLine("Nadchodzi kolekcja.Pamiec:{0}",GC.GetTotalMemory(false));
      else
          Console.WriteLine("Cos poszlo nie tak...");

      GCNotificationStatus gcCompletedStatus = GC.WaitForFullGCComplete(-1);
      if (gcCompletedStatus == GCNotificationStatus.Succeeded)
          Console.WriteLine("Kolekcja zakonczona.Pamiec:{0}", GC.GetTotalMemory(false));
      else
          return;
  }
}

Należy mieć na uwadze, że rozwiązanie dotyczy wyłącznie nierównoległego GC więc należy takowy ustawić w pliku konfiguracyjnym (więcej o concurrent GC w nadchodzącym artykule):

<configuration>
   <runtime>
       <gcConcurrent enabled="false"/>
   </runtime>
</configuration>

W głównym wątku możemy stworzyć pętle, która sztucznie alokuje pamięć, tak abyśmy mogli przetestować metodę MonitorMemory:

class Program
{        
   static void Main(string[] args)
   {
       GC.RegisterForFullGCNotification(maxGenerationThreshold: 10, largeObjectHeapThreshold: 10);
       Task.Factory.StartNew(MonitorMemory);
       while(true)
       {
           byte[]buffer=new byte[85000];
           Thread.Sleep(100);
       }
   }
   private static void MonitorMemory()
   {
       while (true)
       {
           GCNotificationStatus gcStartedStatus = GC.WaitForFullGCApproach(-1);
           if (gcStartedStatus == GCNotificationStatus.Succeeded)                    
               Console.WriteLine("Nadchodzi kolekcja.Pamiec:{0}",GC.GetTotalMemory(false));
           else
               Console.WriteLine("Cos poszlo nie tak...");

           GCNotificationStatus gcCompletedStatus = GC.WaitForFullGCComplete(-1);
           if (gcCompletedStatus == GCNotificationStatus.Succeeded)
               Console.WriteLine("Kolekcja zakonczona.Pamiec:{0}", GC.GetTotalMemory(false));
           else
               return;
       }
   }
}

Na zakończenie kilka uwag. Metody nie gwarantują, że powiadomienie zostanie wysłane zatem nie można projektować algorytmów w ten sposób, że zawsze spodziewają się tego. W celu wstrzymania powiadomień wystarczy wywołać CancelFullGCNotification.

Jaki to ma sens w praktyce? Dla większości aplikacji: żaden. Dla systemów low-latency – nie wiem, nie wykorzystałem jeszcze tego w praktyce i nie wiem czy jest sens. Stworzenie wątku to w końcu skomplikowana sprawa i wiąże się z tym pewne zużycie pamięci.

Wielowątkowość i priorytety

Dzisiaj znów trochę od strony teoretycznej postaram wyjaśnić jak działają priorytety wątków w systemach Windows. Każdy chyba słyszał o tym, że wątkowi można nadać priorytet. Nie każdy natomiast wie, jaki jest zakres oraz jak wygląda dokładnie ich interpretacja.

Przede wszystkim mamy do czynienia z dwoma typami priorytetów, które składają się na końcową wartość używaną przez task scheduler. Pierwszy komponent dotyczy procesu i jest to tzw. klasa priorytetu. W Windows możemy wyróżnić następujące klasy:

 

Nazwa klasy Wartości Domyślna wartość
IDLE_PRIORITY_CLASS 1-6 4
BELOW_NORMAL_PRIORITY_CLASS 4-8 6
NORMAL_PRIORITY_CLASS 6-10 8
ABOVE_NORMAL_PRIORITY_CLASS 8-12 10
HIGH_PRIORITY_CLASS 11-15 13
REALTIME_PRIORITY_CLASS 16-31 24

Wszystkie powyższe klasy dotyczą całego procesu a nie poszczególnego wątku. Domyślnie procesy uruchomione przez Explorer należą do klasy NORMAL_PRIORITY_CLASS. Najwyższa klasa REAL_TIME_PRIORITY przeznaczona jest dla sterowników i innych systemowych programów, które muszą zawsze zostać wykonane i przerywają większość innych wątków. Procesy REAL_TIME mogą przerwać nawet przetwarzanie danych z klawiatury czy z myszki. Z kolei najniższa klasa, IDLE, służy dla programów, które powinny być wykonywane, gdy nie ma nic innego do roboty – np. wygaszacze ekranu.

Z powyższej tabeli również widać, że najwyższy możliwy priorytet to 31.

Drugim komponentem, decydującym o całkowitym priorytecie wątku jest tzw. priorytet relatywny, który dotyczy już specyficznego wątku:

 

Relatywny priorytet Modyfikacja

THREAD_PRIORITY_IDLE

Wartość absolutna:  15 dla realtime, 1 dla dynamic range.

THREAD_PRIORITY_LOWEST

-2

THREAD_PRIORITY_BELOW_NORMAL

-1

THREAD_PRIORITY_NORMAL

0

THREAD_PRIORITY_ABOVE_NORMAL

+1

THREAD_PRIORITY_HIGHEST

+2

THREAD_PRIORITY_TIME_CRITICAL

Wartość absolutna: 31 dla real_time, 15 dla dynamic range

Relatywy priorytet jest zatem przesunięciem względem klasy procesu. Jeśli klasa procesu to HIGH_PRIORITY_CLASS, a relatywny priorytet wątku to THREAD_PRIORITY_BELOW_NORMAL, końcowa wartość będzie równa 13-1==12. Cześć jednak wartości ma charakter absolutny (THREAD_PRIORITY_IDLE oraz THREAD_PRIORITY_TIME_CRITICAL). Końcowa wartość jednak również zależy od klasy procesu. Przedział realtime to wartości od 16-31, a dynamic_range to 1-15.

W systemach Windows istnieje jeszcze priorytet zerowy. Jest on jednak zarezerwowany dla tzw. zero page thread – wątku systemowego używanego do zarządzania pamięcią.

W praktyce, w .NET modyfikujemy wyłącznie relatywny priorytet wątku. .NET nie wspiera najniższego (THREAD_PRIORITY_IDLE) oraz najwyższego relatywnego priorytetu (THREAD_PRIORITY_TIME_CRINICAL). Za pomocą właściwości Thread.Priority możemy ustawić następującą relatywną wartość:

ThreadPriority.Lowest
ThreadPriority.BelowNormal
ThreadPriority.Normal
ThreadPriority.AboveNormal
ThreadPriority.Highest

Należy pamiętać, że scheduler będzie zawsze próbował zaplanować najpierw wątek o wyższym absolutnym priorytecie. Wątki z mniejszym priorytetem będą musiały poczekać aż wszystkie o wyższym zostaną wykonane. Czasami scheduler może samodzielnie zwiększyć priorytet (tzw. priority boost), aby uniknąć na przykład zagłodzenia czy lock convoy – ale o tym kiedyś indziej.

Code Review: Thread.Suspend

Każdy wątek posiada metodę Suspend, która wstrzymuje jego wykonywanie. Ktoś mógłby napisać  takiego “potworka”:

class Program
{        
   static void Main(string[] args)
   {
       Thread thread=new Thread(Run);
       thread.Start();
       
       Thread.Sleep(1000);
       thread.Suspend();
       Thread.Sleep(5000);
       thread.Resume();

   }
   static private void Run()
   {
       while(true)
       {
           Console.WriteLine("Running...");
       }            
   }
}

Używanie Suspend jest bardzo złą praktyką i może spowodować wiele problemów. Od którejś wersji .NET Framework, metoda ta została nawet oznaczona jako Obselete. Dlaczego, jest to takie złe podejście?

Zastanówmy się, po co możemy chcieć zawiesić wątek na kilka sekund. Skoro tworzymy go, chcemy w końcu aby wykonał jak najszybciej swoje operacje. Nie ma sensu wstrzymywania go bo oczywiście efekt będzie taki, że operacja zostanie później wykonana. Ktoś może zasugerować, że być może wątek powinien zostać wstrzymany ponieważ na jakimś etapie, pewne dane mogą być jeszcze niegotowe. Jeśli tak to mamy do czynienia z czystą synchronizacją i jest to skrajnie zły scenariusz użycia Suspend.

Do synchronizacji służą takie klasy jak Semaphore, Monitor, Mutex, ManualResetEvent etc. W przypadku Suspend, największy problem to fakt, że nie wiemy kiedy dokładnie wątek zostanie wstrzymany. Może to zdarzyć się to w każdym w momencie. Co jeśli wątek A zawiesza wątek B, który aktualnie jest w sekcji krytycznej (lock, semafor) M? W takim przypadku, o ile wątek A, chce również uzyskać dostęp do M, będziemy mieli deadlock.

Wątek zawieszony oczywiście wymusza zmianę kontekstu. Problem w tym, że wciąż on zajmuje zasoby. Jeśli taki wątek nie zostanie wznowiony potem, mamy memory leak – zasoby będą w pamięci aż do momentu zamknięcia procesu.

W 99.9% Suspend jest skrajnie złą praktyką. W jakich zatem sytuacjach jest to dopuszczalne a nawet stanowi jedyne rozwiązanie? Głównie narzędzia programistyczne, debuggery itp. korzystają z tego. Klasycznym przykładem jest Garbage Collector, który musi wstrzymać wątki w celu znalezienia obiektów nieosiągalnych. Podobnie robi wiele debuggerów i tym podobnych narzędzi. To co jednak cechuje powyższe przykłady to brak wykonywania kolejnych blokad czy jakieś logiki, bazującej na wspólnych danych. Wszystkie te przykłady zatrzymują wątek tylko po to, aby pobrać jakieś dane od nich – stacktrace, graf obiektów itp. W praktyce  są to skrajne sytuacje i osobiście nie spotkałem się z nimi w pracy – nigdy nie musiałem skorzystać z Suspend.

Porównanie timer’ów w .NET

W .NET istnieje kilka klas, które odpowiadają mniej więcej za to samo. Przyczyna dlaczego nie ma po prostu jednej klasy a kilka, wynika z faktu, że cześć z nich była zaprojektowana z myślą o konkretnej technologii jak np. WinForms. Nie zmienia to faktu, że dla części programistów nie jest to jasne, kiedy używać konkretnego timer’a.

OK, to zaczynamy. W .NET mamy następujące timer’y:

  1. System.Threading.Timer – używamy, gdy chcemy aby nasza operacja była wykonywana w osobnym wątku (z puli). Dlatego, ten timer znajduje się w przestrzeni Threading, co jawnie mówi, że mamy do czynienia z wielowątkowością.
  2. System.Windows.Forms.Timer – callback zawsze wykonywany na wątku UI. Klasa przeznaczona dla WinForms i może zostać umieszczona na formatce.
  3. System.Windows.Threading.DispatcherTimer – odpowiednik powyższego timer’a dla WPF\Silverlight.
  4. Windows.UI.XAML.DispatcherTimer  – tak samo jak 2 i 3 ale dla aplikacji WindowsStore.
  5. System.Timers.Timer – timer jest analogiczny do System.Threading.Timer tzn. wykonuje callback na osobnym wątku (z puli). Jaka jest więc różnica?

Główną różnicą jest SynchronizationObject, właściwość wyeksponowana przez System.Timers.Timer. Domyślnie, zdarzenie elapsed jest wykonywane na wątku z puli. Istnieje jednak możliwość, właśnie za pomocą SynchronizationObject, synchronizacji. Przekazując obiekt synchronizacyjny (np. dowolną formatkę w WinForms), Elapsed będzie wykonywany w kontekście wątku UI. Czy to oznacza, że efekt jest taki sam jak w przypadku System.Windows.Forms.Timer? Nie do końca…

Załóżmy, że chcemy zsynchronizować timer z formatką w WinForms:

System.Timers.Timer timer=new Timer(1000);
timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
timer.SynchronizingObject = this; // this to formatka
timer.Start();

Następnie gdzieś w kodzie wywołujemy Thread.Sleep na 5 sekund w UI Thread. W przypadku System.Timers.Timer, wszystkie wywołania będą kolejkowane i po 5 sekundach po prostu otrzymamy 5 callback’ów (Interval został ustawiony na jedną sekundę). System.Windows.Forms.Timer w momencie gdy nie może wywołać callbacka (bo np. ktoś wykonał Sleep i wątek nie jest zaplanowany) wtedy po prostu taki callback jest ignorowany.

SynchronizationObject to implementacja interfejsu ISynchronizeInvoke, który zawiera metodę Invoke. W momencie, gdy callback ma zostać wykonany to jest wywoływany właśnie przez SynchronizationObjct.Invoke.  W WinForms, każda formatka implementuje ISynchronizationInvoke i może zostać użyta jako obiekt synchronizujący. Koncepcja WPF jednak znacząco się różni (ISynchronizationInvoke to pochodna z WinAPI) i okno w WPF nie implementuje tego interfejsu. Jeśli chcemy wykonać jakąś operację na wątku UI w WPF używamy DispatcherTimer.

Podsumowując, większość timerów zostało stworzonych z myślą o konkretnej technologii. Wyłącznie System.Threading.Timer jest całkowicie neutralny jeśli chodzi o używaną technologie oraz częściowo System.Timers.Timer (wyjątkiem jest SynchronizationObject).

Ponadto istnieje kilka małych różnic. Na przykład klasa System.Threading.Timer jest oznaczona jako sealed i z tego względu nie może oczywiście być rozszerzona. Threading.Timer również wspiera przekazanie stanu lub dueTime co oznacza opóźnienie zanim pierwszy raz callback zostanie wywołany (pozostałe timer’y tego nie potrafią). Powinniśmy jednak się kierować zawsze technologią z jakiej korzystamy (a może chcemy być niezależni od niej) a nie tymi drobnymi różnicami typu przekazanie stanu – z łatwością możemy to samodzielnie zaimplementować.

LINQ–MaxOrDefault

Dzisiaj króciutka notka, ale mam nadzieję, że komuś przyda się. Załóżmy, że mamy kolekcję zawierającą np. DateTime. Naszym zadaniem jest zwrócenie maksymalnej wartości. Za pomocą LINQ łatwo to zrobić:

DateTime[] dateTimeList=/.../;
DateTime maxValue = dateTimeList.Max();

Niestety, w sytuacji gdy kolekcja zawiera 0 elementów dostaniemy wyjątek: “Sequence contains no elements”. Potrzebujemy funkcji MaxOrDefault, która niestety nie istnieje. Mamy do dyspozycji FirstOrDefault, SingleOrDefault, ale nie ma odpowiednika MaxOrDefault. Można oczywiście sprawdzać na początku czy kolekcja zawiera jakiś element:

DateTime[] dateTimeList=new DateTime[0];
DateTime maxValue;
if (dateTimeList.Any())
    maxValue = dateTimeList.Max();
else
    Console.WriteLine("Kolekcja nie ma elementow.");

Rozwiązanie oczywiście bardzo nieeleganckie. W LINQ istnieje funkcja, która nazywa się DefaultIfEmpty. Zwraca ona domyślny element (można również przekazać własną domyślną wartość), jeśli sekwencja jest pusta. Takim sposobem, możemy zwracać np. NULL, gdy kolekcja jest pusta lub maksymalną wartość jeśli jakieś elementy znajdują tam się.

DateTime[] dateTimeList = new DateTime[0];
DateTime? maxValue = null;
maxValue = dateTimeList.Select(d=>(DateTime?)d).DefaultIfEmpty().Max();

if (maxValue == null)
    Console.WriteLine("Kolekcja nie ma elementow.");

Można również pokusić się o napisanie metody rozszerzającej aby mieć w swoim framework’u po prostu MaxOrDefault().

Macierz zależności (Design structure matrix) w nDepend – interpretacja

W poprzednim wpisie przedstawiłem podstawy macierzy DSM. Dzisiaj postaram się wyjaśnić, jakie znaczenie ma to w praktyce. Sam fakt, że macierz pokazuje referencje między różnymi elementami systemu nie jest zbyt ciekawy.

Pierwszą własność, jaką w łatwy sposób możemy odczytać z DSM jest spójność (cohesion). Dokładna wartość spójności zależy od konkretnej metryki. Ogólnie pisząc, wysoka spójność w systemie oznacza, że elementy w nim są bardzo od siebie zależne. Przyjrzyjmy się następującemu przykładowi (źródło: dokumentacja nDepend):

 

Jeśli na macierzy DSM można dostrzec grupy, tworzące kwadrat, wtedy oznacza to, że są one ścisłe ze sobą połączone. W takim przypadku warto zastanowić się nad scaleniem bibliotek w jeden dll i podzielenie komponentów na namespace. Warto zaznaczyć, że obiekty DLL powinny służyć do podziału fizycznego a nie logicznego. Jeśli zatem wiemy, że mamy kilka niezależnych od siebie komponentów, nie oznacza to, że należy je umieścić w osobnych DLL. Podziału logicznego w .NET dokonujemy np. za pomocą przestrzeni nazw. Praca z zbyt wieloma projektami jest kłopotliwa – Visual Studio znacząco spowalnia, deployment jest zwykle trudniejszy. Utrzymanie takich aplikacji jest po prostu bardziej czasochłonne. Kiedy zatem warto umieszczać kod w osobnych bibliotekach? Jak wspomniałem, DLL jest podziałem fizycznym. Jeśli musimy zainstalować dany komponent w kilku różnych instalatorach, wtedy umieszczamy kod w osobnych dll.  Wyobraźmy, sobie, że mamy logikę, która jest dostępna tylko w wersjach premium danej aplikacji. Wtedy oczywiście taka logika, musi być zaimplementowana za pomocą pluginów  aby uzyskać większą elastyczność podczas ładowania obiektów. Jeśli jednak wiemy, że dwa komponenty (mimo, że są niezależne) zawsze muszą być razem zainstalowane oraz istnieje duża szansa, że załadowanie jednego, spowoduje za chwile załadowanie drugiego, wtedy nie ma powodu, aby umieszczać je w osobnych dll, które powodują wiele problemów (wydajność, utrzymanie, jakość pracy). Pamiętajmy jednak, że jeśli nie korzystamy z osobnych AppDomain to wtedy nie ma możliwości usunięcia z pamięci załadowanej już biblioteki. Z tego względu, jeśli komponent B jest używany sporadycznie, a komponent A bardzo często,  warto zastanowić się nad osobnymi bibliotekami lub AppDomain (jeśli oczywiście istnieje uzasadniona potrzeba izolacji kodu).

Wróćmy jednak do spójności. Wysoka spójność oznacza, że komponenty ze sobą są bardzo powiązane. W takiej sytuacji, prawie pewne jest, że chcemy mieć je w jednej bibliotece. Nie ma sensu tworzyć 5 bibliotek, w których wykonywane są podobne operacje i wymagane są relacje do pozostałych dll.

Kiedyś opisywałem zasady SOLID. Jedna z nich mówi o pojedynczej odpowiedzialności.W praktyce dość trudno jednoznacznie określić co to jest pojedyncza odpowiedzialność. Istnieje wiele metryk, które pomagają w tym. Za pomocą DSM jednak, możemy zobaczyć to co powinno być oczywiste – jeśli klasa\komponent A posiada dużo referencji do innych klas, wtedy być może skupia w sobie zbyt wiele odpowiedzialności? Komponenty z zbyt wieloma odpowiedzialnościami są bardzo trudne w utrzymaniu. Wyobraźmy sobie projekt z 100 klas. Jeśli jedna klasa miałaby referencje do pozostałych 99, wtedy modyfikacja czegokolwiek w systemie zmusza nas do przetestowania tej jednej klasy. Przyjrzyjmy się następującej DSM (źródło: dokumentacja nDepend):

Jeśli dany wiersz ma wiele pól zielonych (albo kolumna ma wiele pól niebieskich) wtedy dana klasa ma dużo referencji. Szukamy zatem wierszy\kolumn które mają bardzo wiele zamalowanych pól. Każde takie pole, oznacza kolejną referencję.

W systemach istnieją obiekty niestety, od których wiele klas będzie zależna. Wyobraźmy sobie wszystkie systemowe typy jak String czy Integer. Zmiana kodu w nich, spowoduje, że aplikacje potencjalnie mogą zostać złamane. Warto być świadomym takich obiektów i jeśli zmiana ich jest nieunikniona, wtedy wiadomo przynajmniej jakie inne klasy powinny być przetestowane. Jeśli dana kolumna posiada dużo zielonych pól lub dany wiersz dużo niebieskich pól, wtedy modyfikacja takiego typu, powinna być przeprowadza szczególnie uważnie.

Jeśli nie jest dla Was jasne, kiedy pola są zielone a kiedy niebieskie, zachęcam do przeczytania poprzedniego wpisu. Na początku może to być trochę skomplikowane – mam na myśli identyfikację klas które mają zbyt wiele odpowiedzialności oraz klas, które są używane bardzo często. Proszę zauważyć, że są to przeciwne scenariusze. Zbyt wiele odpowiedzialności zwykle jest złą praktyką, z kolei druga sytuacja zdarza się dość często – jak to ma miejsce w przypadku string. Z tego względu, kolory również odczytujemy odwrotnie. Zachęcam również do wyciągnięcia kartki i narysowania kilku własnych macierzy, które pomogą rozróżnić sytuacje tzw. god class (zbyt wiele odpowiedzialności) oraz po prostu popularny kod(String itp.).

Ostatnim przykładem są cykle, o których napisałem już w poprzednim w poście. Dla przypomnienia tylko dodam, że macierz musi być trójkątna co oznacza, że nie ma cykli. nDepend działa jednak trochę inaczej i musimy po prostu unikać czarnych pól, które oznaczają wzajemne relacje (A->B,B->A).

Macierz zależności (Design structure matrix) w nDepend – wprowadzenie

W dzisiejszym wpisie chciałbym zaprezentować macierz zależności i co można z niej odczytać. Pominiemy aspekty matematyczne – ze względu, że jest to macierz istnieje wiele faktów matematycznych ale myślę, że nie są one najważniejsze dla programisty. Załóżmy, że mamy prostą aplikację składającą się z klienta, warstwy usług, biznesowej oraz DAL. Macierz możemy wygenerować w nDepend – o tym programie kiedyś już pisałem. Umożliwia przede wszystkim liczenie metryk kodu i monitorowanie jakości kodu. Macierz dla opisanej aplikacji, wygląda następująco:

image

 

Macierz można tworzyć dla dowolnych elementów np. typy, biblioteki dll, namespace. W powyższym przypadku pokazano biblioteki. Zielone pola oznaczają ile jest referencji od wiersza do kolumny. Pole (1,1) oznacza, że Client ma jedną referencję do ServiceLayer. Kolejne, że ServiceLayer ma jedną referencję do BusinessLayer. Niebieskie pola z kolei, opisują tą samą zależność ale odczytywaną od kolumny do wiersza.

Generując zatem macierz zależności, możemy określić jak zbudowany jest system. Pierwszą rzecz, na jaką powinniśmy zwrócić uwagę są cykle, których zawsze powinniśmy unikać. Dodajmy zatem dodatkową referencję z DAL do BusinessLayer:

image

Fragment systemu, który zawiera cykl został oznaczony czerwonym kwadratem. DAL ma referencję do BusinessLayer a z kolei BusinessLayer również ma referencję do DAL (biblioteki oznaczone są czarnym kwadratem). Dobrze zaprojektowany system powinien zawsze być macierzą trójkątną, czyli taką, która posiada zera wyłącznie po jednej stronie przekątnej. W nDepend będzie to macierz, która posiada wyłącznie elementy zielone i niebieskie.

Ponadto, można wyświetlić pośrednie zależności:

image

Pośrednio Client posiada oczywiście referencje do BusinessLayer (przez ServiceLayer) oraz DAL (przez ServiceLayer->BusinessLayer).

Teoretycznie macierz przedstawia te same informacje co graf:

image

Dla dużych systemów jednak, graf zwykle jest nieczytelny. Oczywiście, zależy to co chcemy analizować. Jeśli chcemy sprawdzić czy nie mamy cykli w całym systemie, wtedy macierz jest lepsza. Jednak gdy chcemy przyjrzeć się z bliska jakiemuś fragmentowi, wtedy graf jest bardziej przejrzysty.

nDepend posiada rozbudowany system pomocy. Wystarczy najechać na komponent aby zobaczyć jego opis:

image

To samo tyczy się opisu metryk. Naprawdę wiele można dowiedzieć się  o dobrych praktykach czytając dołączoną dokumentację.

Co w kolejnym wpisie? Pokażę, jak macierz powinna wpłynąć na refaktoryzację systemu. Istnieje szereg metryk, które w łatwy sposób mogą być odczytane z macierzy i często powinny być sygnałem do refaktoryzacji kodu.

Różnice między Mutex a Semaphore

W .NET do synchronizacji dostępnych jest wiele mechanizmów. W dzisiejszym wpisie chciałbym przedstawić różnice między semaforem a muteksem. Na pierwszy rzut oka, wyglądają one identycznie. Przykład:

class Program
{
   static void Main(string[] args)
   {
       Mutex mutex = new Mutex();
       
       mutex.WaitOne();
       Console.WriteLine("Sekcja krytyczna tutaj");
       mutex.ReleaseMutex();

       Semaphore semaphore=new Semaphore(1,1);
       semaphore.WaitOne();
       Console.WriteLine("Sekcja krytyczna tutaj");
       semaphore.Release();
   }
}

Pierwsza różnica to fakt, że semafor może dopuścić kilka wątków naraz. W konstruktorze podajemy ile wątków może uzyskać dostęp jednocześnie do danej sekcji. Mutex jest zawsze binarny i dopuszcza wyłącznie jeden wątek.

Jaka jest różnica między semaforem binarnym a muteksem?

Muteks jest bardziej zaawansowanym tworem. Przede wszystkim zapamiętuje, z którego wątku została założona blokada. Jeśli wątek A wywołał WaitOne, to tylko wątek A może wykonać Release. Semafor tego nie robi, spójrzmy na przykład potwierdzający tezę:

class Program
{
   static Mutex _mutex = new Mutex();

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Thread.Sleep(100);
       var t2=Task.Factory.StartNew(Run2);

       Task.WaitAll(t1, t2);            
   }
   static void Run1()
   {
       _mutex.WaitOne();
   }
   static void Run2()
   {
       _mutex.ReleaseMutex();
   }
}

Powyższy kod wyrzuci wyjątek ponieważ nie można ReleaseMutex wywołać z innego wątku niż ten, w którym został wykonany WaitOne. Mutex przechowuje zatem identyfikator wątku. Dla odmiany, poniższy kod, korzystający z semafora, nie spowoduje wyjątku:

class Program
{
   static Semaphore _semaphore=new Semaphore(1,1);

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Thread.Sleep(100);
       var t2=Task.Factory.StartNew(Run2);

       Task.WaitAll(t1, t2);            
   }
   static void Run1()
   {
       _semaphore.WaitOne();
   }
   static void Run2()
   {
       _semaphore.Release();
   }
}

Kolejną zmianą jest rekurencja. Przyjrzyjmy się poniższemu kodu, który próbuje założyć blokadę dwa razy ale w tym samym wątku:

class Program
{
   static Semaphore _semaphore=new Semaphore(1,1);

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Task.WaitAll(t1);            
   }
   static void Run1()
   {
       _semaphore.WaitOne();
       Console.WriteLine("Run1");
       Run2();
       _semaphore.Release();
   }
   static void Run2()
   {
       _semaphore.WaitOne();
       Console.WriteLine("Run2");
       _semaphore.Release();
   }
}

Semafor spowoduje po prostu deadlock, ponieważ aby założyć blokadę w Run2, najpierw trzeba zwolnić ją w Run1, co jest niemożliwe oczywiście. Mutex przechowuje informacje o aktualnym wątku, zatem wie, że w Run2 nie trzeba już zakładać ponownie blokady:

class Program
{
   static Mutex _mutex=new Mutex();

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Task.WaitAll(t1);            
   }
   static void Run1()
   {
       _mutex.WaitOne();
       Console.WriteLine("Run1");
       Run2();
       _mutex.ReleaseMutex();
   }
   static void Run2()
   {
       _mutex.WaitOne();
       Console.WriteLine("Run2");
       _mutex.ReleaseMutex();
   }
}

Mutex przechowuje tzw. recursion count – jeśli inna metoda wchodzi do sekcji krytycznej, z tego samego wątku, po prostu licznik recursion count jest zwiększany i nie powoduje to blokady. Niestety powyższe korzyści wiążą się z pewnym obciążeniem ponieważ należy aktualizować kilka innych pól. Z tego wynika fakt, że jeśli rekurencja nie jest potrzebna wtedy lepiej unikać klasy Mutex.

Klasa ManualResetEventSlim a ManualResetEvent

O klasie ManualResetEvent pisałem  na blogu już wielokrotnie. Prosty przykład:

internal static class Sample
{
   private static readonly ManualResetEvent _manualEvent=new ManualResetEvent(false);

   public static void Main()
   {
       Task.Factory.StartNew(Run);
       _manualEvent.WaitOne();
       Console.WriteLine("Exiting...");
   }
   private static void Run()
   {
       Thread.Sleep(1000);
       Console.WriteLine("Run");
       _manualEvent.Set();
   }
}

Zastosowanie ManualResetEvent oraz AutoResetEvent jest szerokie i obejmuje m.in. testy jednostkowe czy synchronizację wątków. W dzisiejszym wpisie jednak nie o tym – jeśli powyższa konstrukcja jest nieznana lub niezrozumiała w pełni to warto poszukać o tym informacji choćby na tym blogu lub np. na MSDN. Dzisiaj chciałbym pokazać, że istnieje pewna odmiana powyższej klasy nazwana ManualResetEventSlim.

Dlaczego ją stworzono? ManualResetEvent jest bardzo skomplikowaną klasą, która pochłania dużo zasobów. Nie zawsze istnieje potrzeba korzystania z niej i dlatego wprowadzono odchudzoną wersję ManualResetEventSlim. Czy jest zatem sens używania ManualResetEvent, który jest tak bardzo rozpowszechniony? Jaka jest różnica pomiędzy powyższymi klasami?

Skrócona wersja to:

  1. Używaj ManualResetEvent gdy dana operacja będzie trwała długo i co za tym idzie, WaitOne będzie musiał blokować wątek na dłuższy czas.
  2. Używaj ManualResetEventSlim dla krótkich operacji, gdzie blokada nie będzie trwała długo.

ManualResetEvent jest implementacją typowo po stronie kernal’a. A co tym idzie, komunikacja .NET-Kernel musi mieć miejsce, co jest olbrzymim obciążeniem dla CPU i pamięci. Zawsze powinnyśmy preferować rozwiązania czysto .NET, gdzie nie ma komunikacji między światem zarządzanym a niezarządzanym. Oczywiście rozwiązania kernelowe mają przewagę dla długich operacji. Potrafią one bowiem uśpić wątek i zaplanować jakąś inną pracę. Rozwiązania synchronizacyjne bazujące na czystym .NET to tylko zwykły spinning (patrz poprzednie posty jeśli jest to niezrozumiałe). Spinning to nic innego jak zwykła pętla a zatem marnuje ona CPU. Jest to doskonałe dla krótkich operacji bo wtedy nie trzeba komunikować się z kernel, usypiać wątku, planować następnego i w końcu context switching. Dla długich operacji spinning jest  jednak marnowaniem zasobów– wątek nic nie robi tylko wykonuje pustą pętle.

ManualResetEventSlim to rozwiązanie hybrydowe – na początku będzie korzystał z spinningu, a dopiero potem wywoła Monitor.Enter, który z kolei na końcu spowoduje uśpienie wątku i komunikację z kernel. Jeśli mamy świadomość, że operacja potrwa długo wtedy nie ma sensu blokować wątku przez spinning i od razu lepiej skorzystać z ManualResetEvent. W przeciwnym wypadku, ManualResetEventSlim i jego spinning może przynieść bardzo pozytywne skutki. Proszę zauważyć, że ManualResetEventSlim również uśpi wątek po jakimś czasie więc nawet dla długich czasów oczekiwania, klasa nie przyniesie ekstremalnie złych efektów (spinning przez długi czas jest bardzo złym rozwiązaniem). Nie oznacza to oczywiście, że ZAWSZE preferować należy ManualResetEventSlim – jeśli wiemy, że operacja trochę potrwa wtedy nie ma najmniejszego sensu wykonywać pustej pętli.

Sposób korzystania jest praktycznie identyczny:

internal static class Sample
{
   private static readonly ManualResetEventSlim _manualEvent = new ManualResetEventSlim(false);

   public static void Main()
   {
       Task.Factory.StartNew(Run);
       _manualEvent.Wait();
       Console.WriteLine("Exiting...");
   }
   private static void Run()
   {
       Thread.Sleep(1000);
       Console.WriteLine("Run");
       _manualEvent.Set();
   }
}