Dwie uwagi o śpiących wątkachWszyscy znamy doskonale funkcję Sleep, która w Windows API służy do zawieszania działania wątku na określony czas (podawany w milisekundach). Wydawałoby się, że musi to być najprostsza funkcja z tego API, jaką tylko można sobie wyobrazić – bo co może być skomplikowanego w „zwykłej pauzie”? A okazuje się, że jak najbardziej może :)
Używając Sleep – zwłaszcza w swej zwykłej wersji – musimy bowiem pamiętać przynajmniej o dwóch sprawach:
Sleep(INFINITE); sprawi, że właściwie możemy ów wątek wyrzucić do kosza, gdyż nie da się już go odwiesić (funkcja ResumeThread wywołana z innego wątku nic tu nie pomoże).MsgWaitForMultipleObjectsEx albo po prostu GetTickCount wraz z wewnętrzną pętlą komunikatów).Ex funkcji Sleep. Różni się on od oryginału tym, że rozpoczęte przez niego oczekiwanie można przerwać, jeśli sobie tego zażyczymy. Wątek uśpiony przez SleepEx może być przedwcześnie obudzony, gdy otrzyma informacje o zakończeniu asynchronicznej operacji I/O lub asynchronicznego wywołania procedury (APC).Sleep. Jest on bowiem determinowany przez długość tzw. kwantów czasu (time slices), jakie system operacyjny przydziela kolejnym wątkom, by zapewnić złudzenie ich jednoczesnego wykonania. Działanie Sleep polega w rzeczywistości na oddaniu systemowi reszty kwantu czasu, który został przydzielony wątkowi; przestawienie wątku w stan „nieuruchamialności” na podaną ilość milisekund; a następnie na wznowieniu jego pracy, gdy ponownie otrzyma czas procesora od systemowego schedulera. W sumie więc czas uśpienia będzie równy:PozostałyKwantCzasu + ParametrSleep + CzasDoUaktywnieniaWątku
Stąd wynikają dwa wnioski. Po pierwsze, nie powinniśmy nigdy używać Sleep jako sposobu na mierzenie czasu – już poczciwy GetTickCount sprawi się tu znacznie lepiej. Po drugie, wywołanie Sleep(0); jest jak najbardziej dopuszczalne i oznacza przedwczesne zrzeczenie się kwantu czasu, jaki wątek dostał od systemu. W czasach 16-bitowych wersji Windows i wielowątkowości bez wywłaszczała była od tego specjalna funkcja Yield, którą należało często wywoływać, aby przełączanie wątków w ogóle było możliwe. Teraz rzecz jasna nie jest to konieczne, ale nadal może być przydatne dla zasygnalizowania, że nasz wątek nie robi nic pożytecznego, a tylko w brzydki sposób na coś czeka (tzw. busy waiting).
O tych dwóch szczegółach odnośnie funkcji Sleep dobrze jest pamiętać, jeśli nasze wątki chcemy usypiać. Jako programiści Windows możemy się aczkolwiek podbudować tym, że nie mamy przy tym takich problemów jak koderzy piszący pod Linuksem. Tam sleep może być potencjalnie zaimplementowany na sygnałach, co wymaga ostrożności przy stosowaniu go razem z funkcjami alarm i signal.
Przerywanie działania wątkuJednym z powodów używania wątków jest możliwość przerwania wykonywanych przezeń czynności właściwie w dowolnym momencie. Dzięki temu można na przykład wyposażyć aplikację okienkową w magiczny przycisk Anuluj obok paska postępu. Przez to zaś użytkownik ma wrażenie, że – nawet jeśli musi (dłuższą) chwilę zaczekać – nadal jest panem sytuacji :)
Jak można więc przerwać wątek, jeśli zachodzi taka potrzeba? Sposobów jest kilka:
while (!Terminating) { /* ... */ }. Dla porządku warto też (a w niektórych środowiskach trzeba), po ustawieniu rzeczonej flagi na wątek zaczekać. Czyni się to zwykle funkcją z join (‘złącz’) w nazwie: Thread.Join/join w .NET/Javie, pthread_join w POSIX-ie i… WaitForSingleObject w Windows API :)Thread.Interrupt/interrupt. Wówczas przy następnym wejściu do funkcji czekającej w rodzaju Thread.Sleep/sleep rzucany jest wyjątek (Thread)InterruptedException, który odwija stos wątku, kończąc w ten sposób jego działanie. W natywnych platformach nie może być oczywiście dokładnego odpowiednika podobnej operacji, lecz można ją symulować czekaniem na jakimś obiekcie synchronizacyjnym i sygnalizowaniem go, gdy chcemy to czekanie przerwać. W POSIX-ie funkcja pthread_cancel działa aczkolwiek w dość podobny sposób do Interrupt, pozwalając na posprzątanie zasobów w trakcie anulowania wątku.Thread.Abort/stop), w wątku rzucany jest bardzo specjalny wyjątek (ThreadAbortException lub ThreadDeath). Ma on tę cechę, że… przechodzi przez (prawie) wszystkie bloki catch – w .NET właściwie nie można go „zdusić”. To jednak sprawia, że taki wyjątek może wykonać po drodze wszystkie bloki finally, co pozwala zwolnić zawłaszczone zasoby i blokady oraz przywrócić obiekty synchronizujące do właściwego stanu. Występująca w Windows API funkcja TerminateThread takiej możliwości już nam nie daje, przez co jej użycie może prowadzić do różnych kłopotów.Wnioski z tych trzech sposobów są mniej więcej takie, że pisząc kod wykonywany w osobnym wątku powinniśmy zawsze przewidzieć „czysty” sposób na jego zakończenie. Poleganie na przerywaniu lub brutalnym zakańczaniu wątków może się bowiem źle skończyć.
Asynchroniczność kontra wątkiNiekiedy trzeba zrobić coś czasochłonnego: operację, która nie zakończy się od razu, lecz zabierze zauważalny odcinek czasu. Dość często dotyczy to odczytu (lub zapisu) danych z miejsca, które nie musi być natychmiast dostępne: gniazdka sieciowego, międzyprocesowego potoku (pipe) czy w niektórych sytuacjach nawet pamięci dyskowej. Wówczas rzadko możemy pozwolić sobie na „powieszenie” programu na parę(naście/dziesiąt) sekund w oczekiwaniu, aż zlecona operacja się zakończy. W międzyczasie trzeba bowiem wykonywać też inne czynności, z aktualizacją interfejsu użytkownika na czele.
Typowy rozwiązaniem jest wtedy umieszczenie czasochłonnej czynności w osobnym wątku. Zdarza się jednak, że nie jest to jedyne wyjście. Niekiedy – na przykład przy korzystaniu z gniazd sieciowych w Windows API lub dowolnych strumieni w .NET – dysponujemy alternatywnym sposobem, którym jest zlecenie operacji asynchronicznej. Polega ono na żądaniu wykonania danego działania „w tle” wraz ze sposobem, w jaki chcemy odebrać informację zwrotną. W tym charakterze chyba najczęściej stosuje się funkcje typu callback, podawane – zależnie od języka – jako wskaźniki (C/C++), delegaci (Delphi, C#) lub obiekty implementujące ustalone interfejsy (Java). Po zakolejkowaniu takiego żądania program wykonuje się dalej bez żadnych przerw. Gdy zaś operacja zakończy się, nasz callback zostanie wywołany i w nim będzie można pobrać rezultaty zleconego zadania.
Brzmi całkiem nieźle, prawda? Właściwie można by powiedzieć, że to świetny sposób na uniknięcie stosowania tych strasznych wątków ;-) W praktyce trzeba jednak pamiętać o tym, że:
W sumie więc warto pamiętać o tym, że przy wprowadzaniu równoległości trzeba zawsze liczyć z dodatkowymi – nazwijmy to – „kwestiami do rozważenia” :] Unikanie tworzenia wątków za wszelką cenę nie musi zatem być najlepszym wyjściem, skoro koszt rozwiązania alternatywnego bywa podobny.
Synchronizacja wątków w JavieDeweloperzy programujący wielowątkowo zapewne znają klasyczne typy wykorzystywanych przy okazji obiektów. Są to na przykład semafory, sekcje krytyczne (zwane też semaforami binarnymi) czy zdarzenia (events). Wszystkie one służą oczywiście do synchronizacji wątków tak, aby wykluczyć jednoczesny, wykluczający się dostęp do jednego zasobu.
Tego typu obiekty są wykorzystywane jednak głównie wtedy, kiedy mechanizm wątków jest zrealizowany w sposób specyficzny dla systemu operacyjnego - jak choćby poprzez API z Windows lub bibliotekę pthreads z Linuxa. Jeśli jednak mamy szczęście pracować z językiem, którego wielowątkowość jest częścią, wówczas korzysta się zwykle z nieco innych technik.
Taka sytuacja jest na przykład w Javie. Tam każdy obiekt (czyli instancja klasy dziedziczącej z java.lang.Object) może być użyty jako obiekt synchronizujący. Z grubsza działa to w ten sposób, że gdy jeden z wątków zadeklaruje wykorzystanie danego obiektu - przy pomocy słowa kluczowego synchronized - pozostałe nie mogą zrobić tego samego. Taka synchronizacja może odbywać się na wiele (składniowych) sposobów, jak choćby zadeklarowanie całej metody jak synchronizowanej:
W tym prościutkim przykładzie mamy zagwarantowane, że żaden postronny wątek nie wtrąci się w operację inkrementacji ze zwróceniem wartości (która nie jest atomowa) i stan licznika będzie zawsze poprawny.
Tak więc mamy semafory tudzież sekcje krytyczne. A co np. ze zdarzeniami (sygnałami)? Otóż każdy obiekt posiada metody wait i notify, umożliwiające czekanie na powiadomienie z innego wątku i oczywiście wysłanie takiego powiadomienia. Całkiem skuteczne i dosyć proste; naturalnie na tyle, na ile proste może być programowanie wielowątkowe :)
Ale czy oryginalne? Otóż dziwnym trafem na platformie .NET cała sprawa wygląda niemal dokładnie tak samo :) Odwzorowania przytoczonych elementów Javy w C# to odpowiednio: lock (z dokładnością do kilku niuansów), Monitor.Wait i Monitor.Pulse. Sam sposób tworzenia wątków jest zresztą też bardzo bardzo podobny.
Wszelka zbieżność przypadkowa? Zdecydowanie nie. Lecz dobre rozwiązania warto jest przecież rozpowszechniać :]