BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Wielowątkowość a dostęp do współdzielonych zasobów na przykładzie serwera sieci DirectConnect

03-08-2004 18:39 | zooly

Podstawowe problemy, z jakimi zetkniemy się pracując z wieloma wątkami

Na oficjalnych chatach witryny MSDN programiści zalecają omijanie stosowania wielu wątków szerokim łukiem. Dlaczego? Jest to klasa programów, w której bardzo ciężko jest przewidzieć wszystkie nieoczekiwane sytuacje, które mogą zajść w czasie działania aplikacji. Bardzo ciężkie jest także poprawianie (i odtwarzanie) wszystkich zaistniałych błędów, ponieważ nie mamy dużego wpływu na to, w jakiej kolejności i w jaki sposób będą wykonywane poszczególne wątki. Znaczna większość błędów powstałych w takich aplikacjach związana jest z sytuacjami, w których kilka wątków może zmieniać jeden współdzielony zasób. Oto chyba najbardziej popularna z takich sytuacji (Przykład_1, fragment):

public void IncrementCounter()
{
     for( int i = 0; i < 10000000; i++ )
     {
          //Interlocked.Decrement(ref counter);     //bezpieczne
          --counter;     //niebezpieczne
          for( int d = 0; d < 10; d++ )
               ++counter;     //niebezpieczne
               //Interlocked.Increment(ref counter);     //bezpieczne
     }
}

Powyższą pętlę wykonujemy niemal równocześnie w czterech wątkach. Współdzielona zmienna Counter jest typu long. Wynikiem całej operacji powinna być liczba 360000000. Za każdym razem otrzymujemy jednak zupełnie inny, nieprawidłowy wynik. Co sprawia, że poprawnie zapisany kod generuje niepoprawny wynik? C# jest językiem wysokiego poziomu, oznacza to, że każda, nawet najprostsza instrukcja (np. ++counter) przekłada się na kilka instrukcji asemblera (przykład dla języka C++):

mov eax, Counter_typu_int32
inc eax
mov Counter_typu_int32, eax

Tak wyglądałaby ta instrukcja na poziomie asemblera gdyby zmienna Counter była 32 bitowa. W naszym przypadku ma ona aż 64 bity i nie mieści się w całości w żadnym rejestrze procesora, co więcej, jej inkrementacja zajmie o wiele więcej podstawowych instrukcji procesora. Nie jest to zatem instrukcja niepodzielna. Aby w pełni zrozumieć błędy naszych obliczeń należy wziąć pod uwagę charakter wątków w systemie Windows – są one zarządzane na poziomie jądra systemu. Każdy z wątków w danym procesie ma przydzielony kwant czasu, podczas którego może się wykonywać. Gdy ten czas się zakończy, jądro przełącza kontekst i wznawia wykonywanie innego wątku. W sytuacjach, w których występuje wiele odwołań do wspólnego zasobu może się zdarzyć, że wykonamy instrukcję inc i wówczas nastąpi przełączenie do innego wątku. Wznowiony wątek będzie miał do swojej dyspozycji starą wartość zmiennej, ponieważ nie została one jeszcze uaktualniona. Jak należy się bronić przed tego typu sytuacjami? W przypadku, gdy mamy do czynienia ze zmienną 32 bitową możemy zadeklarować ją ze słowem kluczowym volatile. Daje nam ono pewność, że każdy działający wątek będzie miał do swojej dyspozycji możliwie „najnowszą” wersję zmiennej (wiąże się to jednak ze spadkiem wydajności – kompilator nie optymalizuje dostępu do tej zmiennej, do tego nie mamy całkowitej pewności, że wartość ta jest poprawna). Niestety typ long jest 64 bitowy i musimy znaleźć inny sposób na zabezpieczenie naszej aplikacji.

Klasa Interlocked

Klasa ta jest jedną z niewielu w środowisku .NET 1.1 całkowicie bezpieczną nawet, gdy jest ona używana jednocześnie przez wiele wątków. Dostarcza nam ona dwie bardzo przydatne metody: Increment i Decrement. Pozwalają one na bezpieczne zwiększanie i zmniejszanie zmiennej o 1 (niestety możemy je stosować tylko do zmiennych typu int i long). Po odkomentowaniu i zakomentowaniu odpowiednich linijek w kodzie Przykładu 1 zawsze uzyskamy prawidłowy wynik. Co zrobić, jeśli dana zmienna musi być zwiększona o więcej niż 1 lub też pomnożona?

Sekcje krytyczne i typy wartościowe

Sekcje krytyczne są to fragmenty kodu, do których w danym momencie ma dostęp tylko 1 wątek (wzajemne wykluczanie), wewnątrz takich sekcji możemy bezpiecznie modyfikować obiekty współdzielone (eliminują one problem przedstawiony w przykładzie 1, jednakże przy użyciu słowa kluczowego lock możemy je stosować tylko do typów referencyjnych). Przykład_2 – to program, który rozwiązuje nasz problem. Kod wykonywany w każdym z wątków został nieznacznie zmodyfikowany:

object counterLock = new object();
//...
for( int i = 0; i < 10000000; i++ )
{
     //lock( counterLock )     //z tym wynik będzie poprawny
          counter = counter - 2;     
                    
     for( int d = 0; d < 10; d++ )
          //lock( counterLock )     //z tym wynik będzie poprawny
               counter = counter + 2;                         
}

Obiekt counterLock pomaga nam stworzyć sekcje krytyczne. Proces obliczeniowy wygląda w teraz następujący sposób: tylko wątek, który jest w trakcie zmiany wartości zmiennej counter o 2 ma dostęp do zasobu. Nawet jeśli nastąpi przełączenie kontekstu i wznowienie pracy innego wątku to nie będzie on mógł wejść do swojej sekcji krytycznej. Jest to jedne z najprostszych rozwiązań naszego problemu, niesie jednak ze sobą pewne niebezpieczeństwo. Należy wystrzegać się stosowania następującej techniki kodowania:

lock( new object() )
               //sekcja krytyczna

Niestety nie zapewni ona właściwego bezpieczeństwa, gdyż za każdym razem sekcja krytyczna będzie opierała się na innym obiekcie.

Pomimo tego, iż stosowanie standardowych sekcji krytycznych jest bardzo wygodne, nie zawsze musi być ono efektywne. Następujący kod jest fragmentem przykładu 3:

     class Resource
     {
          public void Read()
          {
               //symulacja długiego czasu dostępu do obiektu
               Thread.Sleep(100);
          }
          public void Write()
          {
               Thread.Sleep(100);
          }
     }

Klasa Resource symuluje zasób współdzielony przez 5 wątków. Cztery z nich tylko „odczytują” wartość obiektu, nie modyfikują jej, piąty wątek jest tzw. pisarzem – sporadycznie „modyfikuje” obiekt. Oto kody źródłowe tych wątków:

public void SimulateRead()     //czytelnik
{
     while( !startNow )
          ;
     for( int i = 0; i < 10; i++ )
     {
          lock( sharedResource )
          //RdWrLock.AcquireReaderLock(-1);
               sharedResource.Read();
          //RdWrLock.ReleaseReaderLock();
          }
     }

public void SimulateWrite()     //pisarz
{
     while( !startNow )
          ;
     for( int i = 0; i < 5; i++ )
     {
          Thread.Sleep(200);
          lock( sharedResource )
          //RdWrLock.AcquireWriterLock(-1);
               SharedResource.Write();
          //RdWrLock.ReleaseWriterLock();
     }
}

Wszystkie wątki zostają uaktywnione w momencie zmiany wartości StartNow. Całkowity czas operacji z zastosowaniem standardowych sekcji krytycznych wynosi 5,1 sekundy (na procesorze Athlon 3200+). Możemy zauważyć, że znaczna część wykonywanych operacji to operacje „odczytu” zmiennej współdzielonej. Nie powodują one żadnych zmian wartości obiektu, w każdym wątku czytelnika tracimy zatem ok. 100 ms. Najlepszym rozwiązaniem byłoby udostępnienie obiektu wielu „czytelnikom” na raz i (w celu zmiany wartości obiektu) tylko jednemu - „pisarzowi”. Możliwość taką daje nam klasa ReaderWriterLock. Za pomocą obiektu RdWrLock tej klasy jesteśmy w stanie „ulepszyć” istniejące sekcje krytyczne. W celu uzyskania możliwości odczytu danego zasobu wywołujemy metodę AcquireReaderLock, analogicznie musimy wywołać AcquireWriterLock w celu zapisu danego zasobu. W ten sposób klasa zarządza i poprawia efektywność wykonania naszych wątków. Aby opuścić taką sekcję krytyczną należy wywołać odpowiednio metody: ReleaseReaderLock i ReleaseWriterLock. Kolejną zaletą takiego rozwiązania jest możliwość kontroli czasu, przez który wątek oczekuje na zasób (w naszym przypadku wartość -1 oznacza, że czekamy do skutku, w niektórych sytuacjach możemy zaś nigdy nie uzyskać dostępu do zasobu). Sytuacje, w których wątek czeka „wieczność” i nie może uzyskać dostępu do zasobu gdyż ten przetrzymywany jest przez inny wątek, nazywa się zakleszczeniami (ang. deadlocks).

Zakleszczenia

W trakcie tworzenia aplikacji wielowątkowej musimy bardzo uważać przy stosowaniu sekcji krytycznych. Błędy powstałe w wyniku ich złego zastosowania są bezlitosne – objawiają się zazwyczaj jako „zamrożenie” całej aplikacji. Oto kilka przykładów:

(przykład_4, fragment):

public void DoSomethingSafe()
{
while(true)
{
     Monitor.Enter(a);
          Monitor.Enter(b);
               Monitor.Enter(c);
                    Thread.Sleep(500);
                    if( DisplayOnOnConsole )
               Console.WriteLine("1 wątek uzyskał dostęp do zasobów");
               Monitor.Exit(c);
          Monitor.Exit(b);
     Monitor.Exit(a);
}}

public void CreateDeadLock()
{
while(true)
{
     Monitor.Enter(c);
          Monitor.Enter(b);
               Monitor.Enter(a);
                    Thread.Sleep(500);     
Console.WriteLine("2 wątek uzyskał dostęp do zasobów!");
               Monitor.Exit(a);
          Monitor.Exit(b);
     Monitor.Exit(c);
Thread.Sleep(200);
}
}

Jest to chyba jeden z bardziej popularnych błędów, jakie możemy popełnić, gdy do wykonania procedury wątku potrzebujemy „zablokowania” przynajmniej 2 zasobów. Do sekcji krytycznych, stworzonych za pomocą klasy Monitor wchodzimy w każdym wątku w innej kolejności. Na podobne problemy możemy natrafić, podczas opuszczania sekcji krytycznych w poniższy sposób:

     Monitor.Enter(a);
          Monitor.Enter(b);
               Thread.Sleep(500);     
          Monitor.Exit(a);
     Monitor.Exit(b);

Stwarza to niewątpliwie sytuację, która może powodować zakleszczenia.

Pomimo swoich zalet, stosowanie przedstawionej wcześniej klasy ReaderWriterLock wymaga od nas jeszcze większej czujności. Należy wyłącznie używać par typu:

ReaderWriterLock RdWrLock = new ReaderWriterLock();
RdWrLock.AcquireWriterLock(-1);
     sharedResource.Write();
RdWrLock.ReleaseWriterLock()
RdWrLock.AcquireReaderLock(-1);
     sharedResource.Write();
RdWrLock.ReleaseReaderLock()

W wyniku połączenia obu wersji dostaniemy:

RdWrLock.AcquireReaderLock(-1);
     //rób coś
     //potrzeba modyfikacji zasobu?
     LockCookie lc = RdWrLock.UpgradeToWriterLock(-1);
          //modyfikuj zasób
          //powróć do funkcji "czytelnika"
     RdWrLock.DowngradeFromWriterLock(ref lc);
//zwolnij chęć dostępu do zasobu:
RdWrLock.ReleaseReaderLock();

Jest to wyjątkowo efektywna metoda na przejście z funkcji „czytelnika” do funkcji „pisarza”.

Dodatkowo należy przechwytywać wszystkie wyjątki, jakie mogą się pojawić w samej sekcji krytycznej. Jeśli tego nie zrobimy, wówczas żaden wątek nie będzie mógł uzyskać uprawnień do zapisu tak „zabezpieczonego” zasobu.

Jak można zapobiegać zakleszczeniom?

Wszystko zależy od organizacji wątków w naszej aplikacji. Jeśli dostęp do wspólnych zasobów jest dobrze przemyślany i unikamy przetrzymywania kluczowych obiektów przez dłuższy okres czasu, to zakleszczenia nie powinny się pojawić. Jeśli zaś jesteśmy zmuszeni do tworzenia niebezpiecznych fragmentów kodu, wówczas pozostaje jeszcze jedno, niezbyt wygodne, ale efektywne podejście. W dotychczasowych przykładach stosowałem metodę: „czekaj na zasób do skutku” – to właśnie ona, zastosowana w nieodpowiedni sposób, sprzyja powstawaniu zakleszczeń. W przypadku klasy Monitor zamiast metody Enter możemy użyć TryEnter (zainteresowanym polecam zapoznanie się z poprawioną wersją przykładu 4). Jeśli ustalony przez nas czas oczekiwania minie, wówczas wywołana metoda zwróci wartość false i będziemy mieli możliwość zareagowania na tą sytuację. Podobną możliwość oferuje klasa ReaderWriterLock. Zamiast wartości -1 (oznaczającej nieskończoność) możemy podać okres oczekiwania w milisekundach (o niepowodzeniu zostaniemy w tym przypadku powiadomieni wyjątkiem).

W kolejnej części artykułu przedstawię kolejne zagadnienia związane ze stosowaniem wielu wątków w jednej aplikacji. Wszystkie będą dotyczyły w pełni działającej aplikacji – serwera sieci DirectConnect. Oto niezbędne informacje nt. tej sieci i samego protokołu klient-serwer, zaimplementowanego a aplikacji DCServer.

Sieć DirectConnect

Protokół sieci DirectConnect opracowany został przez Jona Hessa i wydany przez firmę NeoModus. Początkowo była to sieć zamknięta – zarówno klient jak i serwer pochodziły z jednego źródła. Z biegiem upływu czasu, dzięki dokładnemu przeanalizowaniu pakietów wysyłanych i odbieranych w tej sieci, zaczęły powstawać pierwsze zewnętrzne (i do tego pozbawione dodatków typu adware) wersje zarówno serwerów (nazywanych inaczej hubami) jak i klientów. Największa popularność ma obecnie klient DC++ (jego kody źródłowe i opracowany protokół sieci można znaleźć na serwerze SourceForge). Jeśli chodzi o serwery – powstały one w niemal każdym języku programowania, listę najbardziej popularnych znaleźć można na tej stronie.

Szczegóły techniczne

Można powiedzieć, że DirectConnect jest hybrydą serwera ftp i sieci p2p. Serwer jest miejscem, które gromadzi wszystkich użytkowników. Przekazuje między nimi takie informacje jak status klienta, wiadomości typu chat, częściowo pośredniczy w wymianie rezultatów wyszukiwania plików. Wszystkie komunikaty serwera przesyłane są w postaci tekstowej za pomocą protokołu TCP.

Klienci mogą być aktywni lub pasywni (ograniczeni NATem lub firewallem). Przesyłanie plików odbywa się bez pośrednictwa serwera (p2p, klienci komunikują się wtedy zarówno za pomocą protokołu TCP i UDP). Dopuszczalne są dwa scenariusze połączeń klient-klient: aktywny-aktywny i aktywny-pasywny.

Po pobieżnej lekturze protokołu możemy dojść do wniosku, że serwer obciążony jest stosunkowo dużym bagażem zadań. Ściągnięcie pojedynczego pliku przez użytkownika wymaga wysłanie uaktualnionej informacji o nim do wszystkich osób połączonych do serwera. W sytuacji, gdy połączonych jest kilka tysięcy klientów kłopotliwe może być zapewnienie odpowiedniego czasu reakcji serwera. Niezbyt efektywny protokół, brak jakiegokolwiek szyfrowania przesyłanych wiadomości, do tego stosowanie często blokowanego portu 411. Co zatem zadecydowało o ogromnej popularności tego systemu wymiany plików? Najważniejsza jest przede wszystkim możliwość tworzenia swego rodzaju społeczności skupionych na wymianie tylko pewnego rodzaju plików (istnieją całe huby gromadzące miłośników muzyki metalowej, jazzowej itd.). Konieczność współdzielenia ogromnej ilości danych (przeciętnie min. 5GB) powoduje, że zasoby dostępne na wszystkich serwerach wynoszą ok. 1 petabajta!

Implementacja, kolekcje

W DCServerze użyłem następujących kolekcji:

  • Clients (SortedList), ChatWindows typu Hashtable (w klasie Server) – modyfikowana w trakcie logowania nowego klienta (metoda Server.EndLogInClient) i w czasie jego wylogowywania (wątek działający w oparciu o metodę Server.DisconnectThread)

  • ClientsToBeDisconnected typu Queue (w klasie Server) – stosowana jako kolejka nicków klientów, oczekujących na odłączenie (modyfikowana zarówno przez wspomniany wcześniej wątek jak i przez często stosowaną w kodzie metodę Server.EnqueueClientToBeDisconnected)

  • Database typu XmlDocument (w klasie SeverDatabase) – modyfikowana podczas zmiany opcji serwera, rejestrowania nowego użytkownika, intensywnie używana podczas logowania nowego klienta

Wszystkie powyższe kolekcje muszą być w jakiś sposób zsynchronizowane. Jeśli jesteśmy odpowiednio zdyscyplinowani, możemy synchronizować do nich dostęp za pomocą właściwości SyncRoot (niestety klasa XmlDocument nie udostępnia takiej funkcjonalności). Zwraca ona obiekt (niekoniecznie naszą kolekcję), wobec którego możemy użyć Monitora. W większości przypadków wygodniej jednak będzie zastosować tzw. wrappera (opakowaną i zabezpieczoną dla nas wersję kolekcji). Wersję taką możemy uzyskać za pomocą statycznej metody Synchronized (nie musimy robić tego dla typu Hashtable, ale tylko i wyłącznie wtedy, kiedy mamy pewność, że taka kolekcja będzie modyfikowana przez tylko i wyłącznie 1 wątek, odczyt może bezpiecznie być wykonany przez wszystkie wątki i nie stworzy błędów). Stosowanie opakowanych wersji kolekcji daje nam niemal całkowitą pewność, że nie wystąpią żadne błędy. Niestety musimy jeszcze uważać na sytuację zaprezentowaną w przykładzie 5.

Po uruchomieniu programu zobaczymy następujący wyjątek:

Oto wykonujące się wątki:

public void Display()
{
     foreach( DictionaryEntry de in Numbers )
     {
          Console.WriteLine( ((int)de.Value).ToString() );
     }
}

public void Modify()
{
     for( int i = 0; i < Numbers.Count; i++)
     {
          Numbers[i.ToString()] = i;
     }
}

Pomimo iż użyliśmy zsynchronizowanej tabeli hashowanej, przechodzenie przez wszystkie elementy kolekcji nie jest operacją bezpieczną. Hashtable jest słownikiem, zaś każda operacja umieszczenia lub zmiany elementu w kolekcji pociąga za sobą zdezaktualizowanie wszystkich istniejących iteratorów (a to właśnie na nich opiera się iteracja foreach). W takich przypadkach nie mamy wyboru – pozostaje jedynie stosowanie sekcji krytycznych (lub użycie nieco wolniejszej kolekcji SortedList, która umożliwia dostęp zarówno przez indeks jak i za pomocą klucza).

Jeśli zsynchronizowana wersja kolekcji SortedList umożliwia bezpieczną iterację przez całą jej zawartość, to dlaczego w metodzie np. Server.SendToAll użyłem instrukcji foreach? Wiele komunikatów serwera musi być rozesłanych do wszystkich klientów dokładnie raz, uzyskiwanie elementów kolekcji za pomocą metody GetByIndex byłoby bezpieczne ale nie musiałoby dawać pożądanych rezultatów - w ten sposób niektórzy klienci mogliby dostać tą samą wiadomość kilka razy albo ani razu.

Wiele wątków a interfejs użytkownika

Powiadamianie użytkownika o postępach pracy wątków za pomocą GUI – jest to chyba jeden z najciekawszych i najczęściej poruszanych tematów dot. wątków. Któż nie chciałby mieć w swojej aplikacji wielu zmieniających się co chwilę liczników, pasków postępu i wyskakujących co chwilę okienek chatu? Niestety, jedyny bezpieczny sposób to stosowanie metody Invoke (lub BeginInvoke) wobec kontrolki, którą chcemy zmienić (wprawdzie tylko niektóre metody jak np. Show, czy też zmiana widoczności kontrolki powodują poważne błędy – zamrożenie części formy, inne – niekoniecznie, ale błędy tego typu mogą się pojawić po dłuższym czasie działania aplikacji).

Zmusza nas to do tworzenia wielu delegatów i stosowania niezrozumiałych na pierwszy rzut oka wywołań funkcji. Przykład 6 zawiera kilka przykładów (m.in. zmiana wartości kontrolki ProgressBar, przezroczystości okna aplikacji i dynamicznego tworzenia nowych form). O ile odwoływanie się do właściwości i metod klas utworzonych w designerze uchodzi nam na sucho, o tyle tworzenie nowych formularzy już nie. Nowe okna, utworzone w innym wątku niż wątek GUI, do tego bez użycia metody Invoke są „zamrożone” i znikają natychmiast po zakończeniu pracy wątku.

Oto kod wątku, w którym prawidłowo tworzymy nowe formularze:

Form2 f;
for( int i = 0; i < 4; i++ )
{
     f =(Form2)this.Invoke(new CreateNewFormDelegate(this.CreateNewForm));
     //nowe onkno zostało stworzone w wątku GUI
     this.Invoke( new MethodInvoker(f.Show) );
     //dopiero po stworzeniu uchwytu okna (czyli po pokazaniu okna)
     //możemy stosować jego metodę Invoke
     f.Invoke( new SetWindowText(f.SetTitle), new object[]{ "Z Invoke"});
     CreatedForms.Add(f);
     Thread.Sleep(400);
}

Za pomocą delegata: delegate Form2 CreateNewFormDelegate();

wywołujemy metodę:

public Form2 CreateNewForm()
{
     return new Form2();
}

Zdefiniowany w przestrzeni nazw System.Threading delegat MethodInvoker oszczędzi nam wysiłku przy wywoływaniu metody void Form2.Show(). Co oprócz bezpieczeństwa daje nam używanie metody Invoke? – Wygodę, wszystkie kontrolki użytkownika intensywnie używają funkcji Windows API i aby zapewnić całkowite bezpieczeństwo działania kontrolki musielibyśmy w swoim kodzie wszystkie ich metody opakować w sekcje krytyczne. Wywoływanie wszystkich ich metod i właściwości na wątku głównym prawie nigdy nie spowoduje zakleszczenia.

Po raz kolejny użyłem słowa „prawie” – niestety praca z wieloma wątkami jest pełna niebezpieczeństw. Powinniśmy przestrzegać jeszcze jednej reguły: wszystkie metody, których wykonanie może zająć sporo czasu, powinny być wywoływane na wątku innym niż wątek GUI (np. za pomocą wywołań asynchronicznych). Dlaczego? Wątek ten jest w pewnym sensie zasobem krytyczny. Jeśli będziemy go blokowali – może dojść do zakleszczeń a wtedy aplikacja przestanie odpowiadać na akcje użytkownika.

Jak to wszystko działa? – Wątki w DCSerwerze

W większości przykładów, jakie możemy znaleźć w sieci mamy do czynienia z następującym scenariuszem: dla każdego klienta tworzony jest oddzielny wątek. Dla kilku połączeń jest to bardzo wygodne rozwiązanie, ponieważ nie trzeba synchronizować współdzielonych zasobów – zazwyczaj tylko 1 wątek ma dostęp do gniazda (socketu). Niestety w 1 procesie domyślnie może pracować do 25 wątków (możemy zmienić to ograniczenie, ale nie rozwiąże to naszych problemów). Przy tak dobranej organizacji aplikacji może się zdarzyć (zwłaszcza przy dużej ilości połączeń), że więcej czasu procesora tracimy na przełączanie się między wątkami niż na obsługę połączonych użytkowników. Ja zastosowałem następujący model:

  • 1 wątek (Server.Listen) – łączy nowych użytkowników (samo logowanie odbywa się za pomocą wywołań asynchronicznych, modyfikuje kolekcję Clients)

  • 1 wątek (Server.DisconnectThread) – rozłącza klientów, modyfikuje kolekcję Clients

  • 4 wątki (Server.WorkingThread) – każdy z nich pobiera dane z określonych indeksów kolekcji Clients, a następnie je przetwarza (Server.ProcessRequest)

Wszystkie wątki, za wyjątkiem nasłuchującego, są zarządzane przez klasę ThreadPool (umożliwia bardziej wydajne zarządzanie wątkami). Dzięki takiemu zabiegowi mamy nadal możliwość zmiany priorytetu wątku nasłuchującego (klasa ThreadPool nie uwzględnia priotytetów, wybór działającego wątku zależy od dostępności wymaganych przez niego zasobów i operacji Sleep).

W celu rozłączania klientów specjalnie użyłem oddzielnego wątku – dzięki temu, w czasie trwania sekcji krytycznej, w której będziemy modyfikowali współdzieloną kolekcję, możemy usunąć więcej elementów (należy pamiętać, iż w takiej sekcji krytycznej może się znajdować tylko 1 wątek – wątki oczekujące tylko na odczyt kolekcji będą musiały czekać). Przy niewielkiej modyfikacji tego wątku będziemy mogli także szybciej dodawać nowych klientów.

Za obsługę protokołu odpowiada wspomniana wcześniej metoda: Server.ProcessRequest. Korzysta ona z klasy Auxiliary w celu wyodrębnienia odpowiednich argumentów rozkazu (ja wykorzystałem w tym celu wygodne wyrażenia regularne jednakże większość serwerów DirectConnect stosuje zamiast tego zwykłe operacje na stringach takie jak Substring czy FindFirstOf). W związku z tym, że każdy wątek pobierający dane od użytkownika znajduje się w sekcji krytycznej przeznaczonej tylko i wyłącznie do pobierania elementów z kolekcji Clients, pominąłem te sekcje m.in. w metodach: SendTo, SendToAll, SendToAllExcept i SendPrivateMessage.

Wyświetlaniem zalogowanych użytkowników i innych statystyk zajmują się odpowiednie zdarzenia, przypisywane w czasie startu programu. Jest to bardzo wygodne rozwiązanie, jednakże może wprowadzać nieznaczne opóźnienia (dodatkowo kontrolka ListView niezbyt szybko odświeża swoją zawartość).

Podsumowanie

Tworzenie aplikacji typu serwer niewątpliwie obarczone jest ogromną odpowiedzialnością. Gdy dojdzie do zawieszenia klienta niezadowolony będzie 1, góra 2 użytkowników. W przypadku serwera będą to setki, a może i tysiące. W jaki sposób można sprawdzić niezawodność zaprezentowanej przeze mnie aplikacji? Najlepiej poddać ją sporemu „wysiłkowi”. Ww. test przeprowadzić możemy za pomocą programu DCStressor. Umożliwia on szeroki dobór parametrów testu i dwie główne metody: testująca przepustowość i druga – emulująca klientów rozłączających się po krótkich okresach czasu. Cechy, jakie są pożądane przy aplikacjach typu serwer to: pewność działania i efektywność. Jeśli pierwsza z nich została osiągnięta, to możemy próbować polepszyć drugą. Temat optymalizacji aplikacji jest jednak niezwykle szeroki – tym zajmiemy się w kolejnych artykułach.

Załączniki:

tagi: sieci

Komentarze 11

User 79341
User 79341
39 pkt.
Poczatkujacy
21-01-2010
oceń pozytywnie 0
Dobre, praktyczne, omówienie paru problemów przy pracy z wątkami; nota obniżona za Direct Connecta - gdyby był tu opis BitTorrenta... (oczywiście żartuje) ;)
andrzej.ptak7835
andrzej.ptak7835
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Wystepuja pewne niescislosci w rozumieniu idei maszyny wirtualnej .NET - nie mozna oczekiwac od niej, zeby korzystala z rejestrow CPU albo utrzymywala rezim w przetwarzaniu wielowatkowym. Zabraklo bardziej skomplikowanych przykladow, jest za to implementacja serwera sieci p2p z wzmianka o petabajcie dostepnej w niej muzyki - brak moralnosci psuje branze...
User 79209
User 79209
2 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Fajnie, wreszcie cos nowego o watkach, a nie tylko Thread i lock. Nie za bardzo podobaly mi sie klasy z malej litery i nie wiele mowiace stwierdzenie : "Klasa Interlocked jest jedną z niewielu w środowisku .NET 1.1 całkowicie bezpieczną"
User 79550
User 79550
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Podoba mi się pomysł na aplikację wielowątkową, dobrze i treściwie opisany problem zachęcający do dalszej pracy
User 79215
User 79215
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Doceniam jakość artykułu, w tym jego wysoki poziom merytoryczny, dobre przykłady, ale moim zdaniem jego największą wadą jest objętość. Z drugiej strony, nie podjąłbym się napisania artykułu o podobnej dawce wiedzy na dwóch stronach...
uzytkownik usuniety
uzytkownik usuniety
3556 pkt.
Guru
21-01-2010
oceń pozytywnie 0
Artykuł jest bardzo ciekawy. Czyta się go naprawdę przyjemnie. Najgorsze jest jednak szybkie przejście od prostych przykładów do serwera DirectConnect. Szkoda, że został on tak mało omówiony. Poza tym jest bardzo ciekawy. Przykłady są obrazowe. Co więcej artykuł zawiera sporo praktycznych uwag (odnośnie wątków) pomijanych w dokumentacji. Bardzo podobają mi się programy dołączone do artykułu - jest kod, w który można zajrzeć... o czym niestety niektórzy autorzy artykułów zapominają :/, że to w końcu konkurs CODEguru... Szkoda, że wkradły się pewne nieścisłości
User 109825
User 109825
2 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bardzo dobrze omówione tworzenie servera i wielowątkowość! brawo!!
corax1719
corax1719
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
fajne
User 79379
User 79379
9 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
bardzo dobry artykuł, + za ciekawy pomysł z wykorzystaniem serwera DC
will0w
will0w
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bardzo ciekawy artykuł, porusza ważne kwestie związane z programowaniem na wątkach i zwiększeniem wydajności naszych aplikacji. Bardzo przejrzyście i zrozumiale napisany. Jak dla mnie za krótko był opisany sam serwer DC i jego budowa w przykładzie.
:)
:)
3 pkt.
Nowicjusz
:)
21-01-2010
oceń pozytywnie 0
postawiłem 6 za prezentowane przykłady - są bardzo ciekawe! Tekst wydaje mi się za długi(tylko przeleciałem wzrokiem :/)... lecz to jest to, do czego można sie zawsze przyczepic...
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.