Wątki
Przegląd zagadnień
Głównym celem modułu jest zapoznanie uczestnika kursu z problematyką programowania obiektowego wykorzystującego wielowątkowość.
Omówione zostaną podstawowe mechanizmy związane z wątkami - w tym synchronizacja oraz problemy związane z tworzeniem własnych mechanizmów synchronizacji wątków.
Zajęcia zakończą się wykonaniem kilku ćwiczeń.
Wątki
Wątki w aplikacji pojawiają się przeważnie w sytuacji, gdy istnieje konieczność wykonania kilku czynności w tym samym czasie - na przykład dokonywania zmian w interfejsie użytkownika podczas, gdy w tle wykonywane są obliczenia.
Druga sytuacja, w której warto rozważyć wykorzystanie kilku wątków ma miejsce na przykład podczas odczytywania danych z pliku - procesor mógłby w tym samym czasie wykonywać jeszcze jedno działanie, dzięki czemu wydawałoby się, że program działa szybciej.
Język C# zawiera przestrzeń nazw System.Threading, która pozwala sprawnie uruchamiać nowe wątki i nimi zarządzać.
Wraz z podjęciem decyzji o zastosowaniu kilku wątków w aplikacji, warto rozważyć, czy nie pociągnie to za sobą spadku jej wydajności - na przykład równoległe uruchomienie dwóch wątków liczących skomplikowane wartości może się okazać gorszym rozwiązaniem niż obliczenie ich po kolei (sytuacja dotyczy maszyny jednoprocesorowej).
Uruchamianie wątków
Wbudowana klasa System.Thread pozwala na łatwe utworzenie nowego wątku. Klasa delegata ThreadStart wskazuje na określoną przez programistę metodę, co pozwala na utworzenie wątku i przekazanie do niego informacji o wywołaniu tej metody w momencie rozpoczęcia działania.
Po utworzeniu wątku pozostaje go jeszcze uruchomić przy pomocy metody Start().
Uruchamianie wątków - przykład
Dwie funkcje - funkcja1 oraz funkcja2 - przygotowane w programie, mogą posłużyć jako metoda wywoływana w wątku. Na przedstawionym fragmencie kodu tworzymy dwa nowe wątki - pierwszy związany z funkcja1, drugi natomiast z funkcja2 - a następnie uruchamiamy je.
Proces zaowocuje naprzemiennym wykonywaniem instrukcji zawartych w funkcja1 oraz w funkcja2.
W większości przypadków wątki samoczynnie wygasają po wykonaniu wszystkich instrukcji zawartych w funkcjach.
Wątki - dalsze informacje (1)
W przypadku, gdy jeden z wątków (w2) potrzebuje danych z innego wątku (w1), warto rozważyć połączenie wątków. Służy do tego kolejna metoda z klasy System.Thread - Join().
Wykonanie instrukcji w2.Join() w metodzie wątku w1 spowoduje zatrzymanie wątku w1 do czasu zakończenia działania wątku w2.
Sposób ten można wykorzystać również do zakończenia programu dopiero po wykonaniu wszystkich wątków - jedno z bardziej klasycznych podejść proponuje umieszczenie wątków kolekcji, a następnie użycie poznanej na jednych z wcześniejszych zajęć instrukcji foreach, która może zostać wykorzystana na przykład do napisania pętli przechodzącej po wszystkich wątkach w kolekcji, która wykonuje łączenie wątków.
Wątki - dalsze informacje (2)
W języku C# przewidziano także przydatny mechanizm zatrzymania wątku na pewien czas, co może przyspieszyć pracę programu na przykład w sytuacji, w której jeden z wątków odpowiada za podawanie czasu, drugi natomiast wykonuje obliczenia. Pierwszy z nich można ustawić na sprawdzanie czasu co minutę, pozostały czas pracy przypisując drugiemu.
Zaowocuje to pominięciem wielu zbędnych przełączeń pracy procesora - czas z reguły wystarczy sprawdzać co kilkanaście sekund.
Mimo, że wątki samoczynnie giną po wykonaniu swego zadania, możliwe jest zakończenie czasu życia wątku na specjalne żądanie programisty.
Najbardziej elegancką wersją takiego działania jest ustawienie opcji KeepAlive, którą można okresowo sprawdzać, po zmianie opcji wątek może się wówczas sam zatrzymać.
Druga z metod to Thread.Interrupt - żąda ona od wątku natychmiastowego zatrzymania działania.
Ostatnia możliwość to wykorzystanie metody Thread.Abort, która zaowocuje zgłoszeniem wyjątku ThreadAbortException. Wyjątek ten trzeba oczywiście obsłużyć w odpowiedni sposób.
Słowo o synchronizacji (1)
Wielowątkowość jest bardzo przydatnym narzędziem konstrukcji programu, niesie za sobą jednak pewne zagrożenia.
Jednym z nich jest sytuacja, w której tylko jeden wątek może mieć dostęp do elementu - na przykład zapisywanie danych do pliku jest zdarzeniem, które może być obsługiwane tylko przez jeden wątek naraz.
Synchronizacja to najprościej mówiąc sposób zarządzania działaniem wątków.
Słowo o synchronizacji (2)
Projektanci języka C# przewidzieli trzy możliwości obsługi synchronizacji.
W prostych przypadkach, gdy programiście potrzebne jest zwiększenie lub zmniejszenie pewnego licznika, dobrym pomysłem jest wykorzystanie metod Increment oraz Decrement klasy Interlocked. Mają one wbudowaną obsługę synchronizacji.
Widoczny na slajdzie kod ilustruje sposób ich użycia.
Bardziej ogólny sposób opiera się na mechanizmie blokady. Główną jego ideą jest oznaczenie sekcji krytycznej w kodzie, do której dostęp będzie miał jednorazowo tylko jeden wątek.
Ostatnia metoda bazuje na mechanizmie klasy Monitor - przewidziano jej użycie we wszystkich przypadkach, w których pierwszy i drugi sposób z jakichś względów nie dostarcza dostatecznie silnego narzędzia.
Synchronizacja - przykład blokady
Powyższy przykład ilustruje metodę Zwiekszenie, w której zastosowano mechanizm blokady dla fragmentu kodu odpowiedzialnego za zwiększanie licznika.
Blok instrukcji ograniczony przez lock(this) jest dostępny w tym samym czasie tylko dla jednego wątku, dzięki czemu nie ma problemu z wartością licznika.
Synchronizacja - monitory - idea
Mechanizm monitorów jest znacznie bardziej skomplikowany od dwóch poprzednich. Metody dołączone do klasy Monitor umożliwiają zarówno wyznaczenie początku (Enter()) i końca (Exit()) synchronizacji, jak i kontrolę kolejności wykonywania wątków (Wait()) czy informowania wątków o możliwości ponownego rozpoczęcia działania (Pulse()).
Problemy synchronizacji
W języku C# istnieją bardzo dobre mechanizmy obsługi wątków. Niezależnie od nich programista ma możliwość przygotować własne.
W procesie tworzenia takiego mechanizmu warto przemyśleć proces obsługi wątków tak, by uniknąć sytuacji wyścigu i zakleszczenia.
Wyścig ma miejsce wtedy, gdy właściwe wykonanie się programu jest uzależnione od niekontrolowanej kolejności wykonania wątków w programie - w celu uniknięcia problemu warto łączyć wątki.
Zakleszczenie to sytuacja, w której dwa wątki wzajemnie oczekują na zakończenie wykonywania zadania. Zapobieżenie takiej sytuacji jest możliwe poprzez zwalnianie i zajmowanie wszystkich zasobów lub pozwalanie wątkom na zajmowanie jak najmniejszych fragmentów kodu.
Podsumowanie
Wątki pozwalają na wykonywanie kilku zadań w jednym czasie. Warto pisać aplikacje wielowątkowe szczególnie na maszyny wieloprocesorowe - pozwoli to często na znaczne przyspieszenie pracy programu.
Synchronizacja jest mechanizmem zarządzania pracą wątków; często pomaga w zachowaniu porządku w programie.
Pytania sprawdzające
Pytania sprawdzające:
1. Dlaczego warto stosować wątki?
2. W jaki sposób można zapewnić synchronizację licznika przy dwóch różnych wątkach?
3. Wymień metody synchronizacji.
Laboratorium
1. Napisz program, który będzie wykonywać jednocześnie dwa zadania: liczyć od 0 do 1000 oraz od 1000 do 0.
2. Przygotuj aplikację, w której dana będzie kolekcja czterech wątków wykonujących się kolejno po sobie. Program ma zakończyć działanie dopiero po wykonaniu zadania przez ostatni wątek.
3. Napisz program, który symulowałby obsługę czytelników przez bibliotekarkę, pamiętając, że jednorazowo bibliotekarka może obsłużyć tylko jednego czytelnika.
L
L