Home > .NET 4.0, C# > [PL] .NET 4.0 – What’s new – Concurrent Collections #2

[PL] .NET 4.0 – What’s new – Concurrent Collections #2

January 10, 2010 Leave a comment Go to comments

Od czasu gdy po raz pierwszy wspomniałem, o czymś nowym w .NET 4.0 minęło już parę miesięcy. Dzisiaj chciałbym przyjrzeć się bliżej innemu, bardzo miłemu “narzędziu”, które zostało dodane do środowiska .NET 4.0. Jak wiadomo, a może i nie .NET 4.0 wprowadza wiele zmian i udogodnień do samego CLR jak i C#. Wszystkie informacje na ten temat znaleźć można w opisie .NET 4.0 Beta 2 na tej stronie. Dosyć spory postęp dokonany został w dziedzinie wielowątkowości – tej, która przydaje mi się najbardziej w pracy. W tym, krótkim wpisie, chciałbym omówić zupełnie coś co według mnie zasługuje na uwagę.

Dotychczas, .NET pomagał programiście pisać programy wielowątkowe wykorzystując różnego rodzaju znane nam mechanizmy zabezpieczające przed jednoczesnym dostępem do obiektu z poziomu dwóch różnych wątków. Mechanizmy te omawiane już na początkowym etapie nauczania na studiach pozwalały na łatwe wykorzystywanie wielowątkowości i omijanie problemów z nią związanych. Oczywiście tutaj nasuwają się już dwa pytania. Czemu ‘pozwalały na łatwe wykorzystanie’? oraz Czy wszystko było takie idealne? Odpowiedź na drugie pytanie brzmi – żaden większy program, który jest pisany nie jest idealny – zawsze pojawiają się błędy, a stają się one tym większe im program jest bardziej skomplikowany, a jeszcze większe, gdy wykorzystujemy wielowątkowość. Wszystkie problemy tego typu są winą – programisty, czyli człowieka jak by ktoś nie wiedział. Jeżeli chodzi o pierwsze pytanie – to odpowiedź jest również banalna. Kiedyś najlepszym językiem programowania był TurboPascal, C, C++. Mi zawsze informatyka i nowinki z świata języków programowania kojarzą się z piramidą Maslowa. Po osiągnięciu pewnego celu zaczynamy pragnąć osiągnąć coś więcej, co prawda możemy się bez tego obyć, ale zawsze lepiej jest pójść dalej. .NET poniżej wersji 4.0 (ten czysty .NET bez dodatków instalowanych) pozwalał na w miarę proste tworzenie aplikacji wielowątkowych. Kolejny poziom naszej ‘piramidy’ osiągnięty został w wersji 4.0.

Przejdźmy do rzeczy i zastanówmy się nad pewnym problemem i jego rozwiązaniem. Jak prawie zawsze nawiązywać będę do elementów wziętych z życia, np.. z pracy w Banku. Wyobraźmy sobie, że mamy jakąś aplikację, która wertuje co pewien czas dane klientów – mogą to być np. wnioski kredytowe. Nasz program co określony cykl ładuje te wnioski do obiektu typu Queue czy List (jak kto woli) i operuje sprawdzając różnego rodzaju operacje – czy nastąpił przelew, czy klient podpisał wniosek, czy klientowi anulowano wniosek. Aby usprawnić operacje wykonywane, wnioski opracowywane są w różnych wątkach – bo chcemy, aby sprzęt był w dużym stopniu wykorzystywany, jak i chcemy przyspieszyć nasze operacje.

Na początek wyjaśnię jak wygląda sposób obsługi klientów w naszym przykładzie. Normalnie moglibyśmy ładować klientów z zewnętrznej bazy danych, czy zewnętrznego systemu – my utworzymy sobie ustaloną liczbę obiektów, w których przechowamy parę nieistotnych informacji. Wszystkich klientów wrzucamy na nasz ‘stos’ – zależenie od testu – z wersji .NET 2.0 czy .NET 4.0. W kolejnym kroku tworzymy ustaloną liczbę wątków, np. 5 i w każdym wątku uruchamiamy procedurę obsługującą wniosek ze stosu. Zależnie od typu kolejki mamy dwie metody – pierwsza operująca na Queue, a druga na ConcurrentQueue.

static void QueueProcess(object param)

 

static void ConcurrentQueueProcess(object param)

Celem, który sobie wyznaczyłem w tym teście, to sprawdzenie jak wygląda obsługa takiego stosu oraz czy nie zmniejsza to nam wydajności systemu obsługującego wnioski (chodzi mi tutaj jak zwykle o czas wykonania).

Jak wygląda zatem blokowanie i obsługa poszczególnych kolejek? Kod dla pierwszej metody – blokujący kolejkę dla wątku i sprawdzający czy coś jest na stosie wygląda następująco:

lock (q)

{

  if (q.Count == 0) return;

  person = q.Dequeue();

}

Dla drugiej metody, analogiczna funkcjonalność zapisuje się tak

if (!cq.TryDequeue(out person)) return;

W obu przypadkach chcemy tego samego – ściągnąć ze stosu obiekt, jeżeli stos jest pusty zakończyć działanie procedury. Jak zauważyć można w pierwszym przypadku blokuję obiekt na wyłączność wątku metodą lock, a w drugim odwołuję się bezpośrednio do stosu metodą TryDequeue. Druga metoda ma jednak ciut więcej zalet – jest krótsza, a także nie musimy pamiętać o blokowaniu obiektu, bo sama klasa robi to za nas, a informację o sukcesie pobrania otrzymujemy w wyniku funkcji.

Jak wobec tego wygląda wydajność? Wykonałem prosty test polegający na zapełnieniu stosów obiektami (do 500 sztuk), rozdzieleniu ich na wątki, a następnie obsłudze polegającej na wykonywaniu jakichś ustalonych operacji. Następnie 10 razy wykonałem dwie metody operujące na poszczególnych typach kolejek. Pod koniec wyliczyłem średni czas wykonania poszczególnych metod, poniżej są wyniki:

Queue (.NET 2.0) ConcurrentQueue (.NET 4.0)
5154,2 ms 5077,6 ms

Jak widać, można powiedzieć, że operowanie na ConcurrentQueue w naszym przypadku było szybsze średnio o 100 ms – prawie nic, ale warto zauważyć, że .NET 4.0 zostało w dużym stopniu przepisane i przeznaczone na operacje wielowątkowe (jest ot inny CLR). Nie znaczy to także, że tylko to posiada .NET 4.0b2 – mój przykład był małą kroplą w oceanie, która różniła się tylko zamianą klas.

Projekt przykładowy:

http://cid-e2eed297c7a63060.skydrive.live.com/self.aspx/Publiczny/NET%204.0/ConcurrentQueue/ConcurrentDemo.zip

  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: