W pierwszej części pisałem o podstawach wątków. Były tam poruszane takie tematy jak tworzenie wątków, ich blokowanie, a także współdzielenie stanu. Dzisiaj postaram się pogłębić ten temat i zwrócić uwagę na bezpieczeństwo wątków.
Stan współdzielony vs. stan lokalny
W ostatnim przykładzie części pierwszej pisałem o stanie współdzielonym wątków. Wrzuciłem tam taki przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class WorldOfThreads { static int from = 1; static int to = 10; static void Main() { Thread cubeThread = new Thread(PrintCubes); Thread squaresThread= new Thread(PrintSquares); squaresThread.Start(); cubeThread.Start(); for(; from <= to; ++from) { Console.WriteLine("Hello world of threads"); } } static void PrintSquares() { for(; from <= to; ++from) { Console.WriteLine(from*from); } } static void PrintCubes() { for(; from <= to; ++from) { Console.WriteLine(from*from*from); } } } |
Jak w łatwy sposób zmienić powyższy kod, by stan ze współdzielonego stał się lokalnym? Najpierw zastanówmy się dlaczego wątki współdzielą stan. Jest to efekt działania dwóch elementów. Po pierwsze zarówno metody które chcemy wywołać jak i zmienna stanowiąca o ich stanie są składowymi statycznymi klasy WorldOfThreads. Po drugie wszystkie są wywoływane jako składowe tej samej klasy (co zresztą implikuje powód pierwszy). Musimy ten stan rzeczy odwrócić. Najpierw należy usunąć słowo kluczowe static z każdego miejsca oprócz nagłówka metody Main. Następnie metody PrintCubes i PrintSquares należy podawać do wątków, jako składowe osobnych instancji klasy WorldOfThreads. Ostatni krok to zrobienie czegoś z pętlą w metodzie Main. Możemy albo nadpisać pole from w metodzie Main, albo wynieść całą pętlę do osobnej metody i wywołać ją z nowego obiektu WorldOfThreads. Zdecydowałem się na to drugie rozwiązanie – zobaczmy kod po tych transformacjach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class WorldOfThreads { int from = 1; int to = 10; static void Main() { Thread cubeThread = new Thread(new WorldOfThreads().PrintCubes); Thread squaresThread= new Thread(new WorldOfThreads().PrintSquares); squaresThread.Start(); cubeThread.Start(); new WorldOfThreads().Greeting(); } public void PrintSquares() { for(; from <= to; ++from) { Console.WriteLine(from*from); } } public void PrintCubes() { for(; from <= to; ++from) { Console.WriteLine(from*from*from); } } public void Greeting() { for(; from <= to; ++from) { Console.WriteLine("Hello world of threads"); } } } |
Teraz wszystko jest w porządku, tzn. nie możemy zdeterminować która metoda pierwsza zakończy klasę, ale mamy pewność że każda z nich zrobi swoje od początku do końca 🙂
Co w przypadku gdy nie jesteśmy w stanie uniknąć stanu współdzielonego?
W powyższym przykładzie dosyć łatwo mogliśmy sprawić by wątki przestały współdzielić stan. W większości sytuacji jesteśmy wstanie tak napisać rozwiązanie by stan był lokalny. Nie mniej mogą się zdarzyć przypadki gdy jest to niemożliwe, albo przynajmniej bardzo trudne (np. w danym miejscu korzystamy z Singletonu). W takich przypadkach możemy skorzystać z mechanizmu tzw. blokad. Blokady to dosyć szeroki i nie najłatwiejszy temat, dlatego poświęcę mu kiedyś osobny wpis, a póki co tylko wspomnę o nim by pamiętać że coś takiego istnieje. Jest to technika pozwalająca na koordynowanie działania wątków w taki sposób, by przy stanie współdzielonym otrzymać deterministyczny wynik.
Rozróżniamy dwa rodzaje blokad. Blokady wykluczające i blokady bez wykluczenia. Różnica polega na tym, że blokady wykluczające działają w taki sposób że, na kodzie/danych które obejmują pozwalają pracować tylko jednemu wątkowi. Korzystając z blokad bez wykluczenia możemy określić liczbę wątków którym chcemy pozwolić pracować na danym kodzie.
Więcej o tym napiszę za jakiś czas, nie mniej na koniec przykład, przedstawiający jak mniej więcej wygląda zakładanie blokad. W roli głównej podstawowy konstrukt jeżeli chodzi o blokady wykluczające – lock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class ImportanThingsCalculator { private static bool IsDone; public static void PrintCalculation() { if (IsDone) { Console.WriteLine("Already done."); return; } int a = 0; int b = 1; for (int i = 0; i < 20; i++) { Thread.Sleep(100); int temp = a; a = b; b = temp + b; char c = i < 19 ? ',' : '.'; Console.Write($"{a}{c} "); } Console.WriteLine(); IsDone = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Program { static void Main(string[] args) { Thread firstThread = new Thread(ImportanThingsCalculator.PrintCalculation); Thread secondThread = new Thread(ImportanThingsCalculator.PrintCalculation); firstThread.Start(); secondThread.Start(); ImportanThingsCalculator.PrintCalculation(); Console.ReadKey(); } } |
Jak możemy się domyślić, zamysłem autora klasy ImportantThingsCalculator było jednorazowe wypisanie efektu „ważnych obliczeń” na ekranie. Niestety autor tego API nie przewidział, że ktoś może użyć go w sposób jaki widać na drugim listingu. Metoda PrintCalculation zostaje wywołana 3 razy – mamy też 3 wątki. Po wykonaniu powyższego, na wyjściu widzimy śmieci w stylu:
1 2 3 4 |
1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 5, 5, 5, 8, 8, 8, 13, 13, 13, 21, 21, 21, 34, 34, 34, 55, 55, 55, 89, 89, 89, 144, 144, 144, 233, 233, 233, 377, 377, 377, 610, 610, 610, 987, 987, 987, 1597, 1597, 1597, 2584, 2584, 2584, 4181, 4181, 4181, 6765. 6765. 6765. |
Podczas gdy oczekiwane wyjście to:
1 2 3 |
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765. Already done. Already done. |
Z pomocą przychodzi nam konstrukcja lock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class ImportanThingsCalculator { private static readonly object blocker = new Object(); private static bool IsDone; public static void PrintCalculation() { lock (blocker) { if (IsDone) { Console.WriteLine("Already done."); return; } int a = 0; int b = 1; for (int i = 0; i < 20; i++) { Thread.Sleep(100); int temp = a; a = b; b = temp + b; char c = i < 19 ? ',' : '.'; Console.Write($"{a}{c} "); } Console.WriteLine(); IsDone = true; } } } |
Przy tej wersji klasy ImportantThingsCalculator dostaniemy spodziewany output. Zobaczmy co się zmieniło. Po pierwsze jako składowa pojawił się obiekt, który nazwałem blocker. Jak widać nie ma w nim żadnej magii zobaczmy więc jego zastosowanie. Całe ciało metody PrintCalculation, zostało umieszczone w bloku lock(blocker). Obiekt synchronizacji (w tym przypadku nasz blocker) istnieje po to by wątek mógł na nim założyć blokadę. Na raz tylko jeden wątek może blokować dany obiekt, więc pozostałe wątki które próbują to robić muszą czekać na zwolnienie blokady (czyli de facto zakończenia pracy wątku). Nie mniej na ten temat napiszę więcej we wpisie poświęconym blokadom wykluczającym.
To był dosyć długi wpis poświęcony praktycznie w całości problemom związanym ze stanem współdzielonym wątków i zaletom płynącym ze zmiany stanu na lokalny. Jak już wspominałem blokady doczekają się ode mnie osobnego wpisu(albo dwóch ;)) natomiast w kolejnej części dotyczącej wątków napiszę o priorytetach wątków, oraz o podstawach tzw. sygnalizowania.