Wielowątkowość – podstawowe informacje

Pojęcie wielowątkowościwielowątkowośćwielowątkowości oznacza złożoną pracę systemu operacyjnego. Dzięki temu w ramach jednego procesuprocesprocesu może zostać wykonane kilka zadań, zwanych wątkami. W e‑materiale wyjaśnimy, czym charakteryzują się wątki w procesie.

Definicja procesu

Pojedyncza instancja programu to proces, który może inicjować inne procesy wzajemnie się ze sobą komunikujące. W każdym z nich musi zachodzić co najmniej jeden wątek, choć może być ich nawet kilkadziesiąt czy kilkaset – w zależności od potrzeb i możliwości maszyny. Wszystkie programy, które tworzyliśmy w innych e‑materiałach, powstawały w ramach jednego wątku i jednego procesu.

W momencie, gdy uruchamialiśmy napisany przez nas program, tworzyliśmy proces, w ramach którego wątek sekwencyjnie wykonywał kolejne wprowadzone przez nas polecenia.

Podstawowe informacje o wątkach w procesie

Wprowadzenie do procesu większej liczby wątków we współczesnych komputerach pozwala na przyspieszenie działania programów – wynika to z efektywniejszego wykorzystywania zasobów procesora. Nie będziemy jednak dociekać, dlaczego stosowanie wielu wątków jest szybsze, ponieważ musielibyśmy opowiedzieć o architekturze procesorów, co nie jest tematem tego e‑materiału. W tym momencie musimy wiedzieć, że w każdym procesie system operacyjny może uruchomić wiele wątków, realizujących osobne zadania. Wątki wykonywane są jednocześnie (współbieżnie). W przypadku gry, którą będziemy projektować w kolejnych e‑materiałach, osobne wątki posłużą nam do obsługi zadań takich jak sterowanie, czy wyświetlanie węża na ekranie.

Wątki, co do zasady, współdzielą pamięć w ramach jednego procesu. Oznacza to, że liczba wcześniej zmodyfikowana przez jeden wątek, w innym wątku będzie poprawnie odczytywana – program pobierze już nową wartość. Jednak bez poprawnie zaimplementowanych mechanizmów synchronizacjimechanizm synchronizacjimechanizmów synchronizacji możemy spowodować tzw. wyścigwyścigwyścig. Załóżmy, że mamy dwa wątki, które pracują na wspólnej puli zmiennych. Oba wątki wykonują się w tym samym czasie.

Linia 1. a znak równości 0. Linia 2. b znak równości 2. Linia 3. c znak równości a plus b. Linia 4. c znak równości c plus b. Linia 5. wypisz c.

Rozważmy to na zaprezentowanym przykładzie. W programie jednowątkowym wynik przedstawionego algorytmu jest oczywisty, na wyjściu zostanie wypisana liczba 4.

Jednak gdy dwa wątki równocześnie wykonują operacje na tych danych, wtedy wynik nie będzie jednoznaczny i możliwe, że będzie się różnił przy każdym uruchomieniu programu.

Nazwijmy nasze wątki AB, a zapis A:N będzie oznaczać, że wątek A wykonuje n‑tą linijkę, np. A:3 oznacza, że wątek A wykonał linijkę trzecią.

Przeanalizujmy działanie programu:

Linia 1. A dwukropek 1 a znak równości 0. Linia 2. B dwukropek 1 a znak równości 0. Linia 3. A dwukropek 2 b znak równości 2. Linia 4. B dwukropek 2 b znak równości 2. Linia 5. A dwukropek 3 c znak równości 0 plus 2 znak równości 2. Linia 6. B dwukropek 3 c znak równości 0 plus 2 znak równości 2. Linia 7. A dwukropek 4 c znak równości 2 plus 2 znak równości 4. Linia 8. B dwukropek 4 c znak równości 4 plus 2 znak równości 6. Linia 9. A dwukropek 5 wypisz 6. Linia 10. B dwukropek 5 wypisz 6.

Wynikiem programu, który wykonywałby się w taki sposób, byłoby wypisanie na ekranie dwóch wartości 6. Taka sytuacja (idealnie synchronicznego działania obu wątków) jest nieosiągalna bez zastosowania mechanizmów synchronizacji. W praktyce bowiem niezsynchronizowane wątki będą walczyć o zasoby, przez co równie dobrze program mógłby wyglądać tak:

Linia 1. A dwukropek 1 a znak równości 0. Linia 2. A dwukropek 2 b znak równości 2. Linia 3. A dwukropek 3 c znak równości 0 plus 2 znak równości 2. Linia 4. A dwukropek 4 c znak równości 2 plus 2 znak równości 4. Linia 5. A dwukropek 5 wypisz 4. Linia 7. B dwukropek 1 a znak równości 0. Linia 8. B dwukropek 2 b znak równości 2. Linia 9. B dwukropek 3 c znak równości 0 plus 2 znak równości 2. Linia 10. B dwukropek 4 c znak równości 2 plus 2 znak równości 4. Linia 11. B dwukropek 5 wypisz 4.

Lub w dowolnej innej kombinacji, w której – w ramach jednego wątku – kolejne instrukcje wykonują się po kolei.

Wyścig może jednak zakończyć się negatywnymi skutkami, gdy wątki wzajemnie nadpiszą swoje wyniki.

Załóżmy, że w procesorze jednocześnie wykonuje się A:3B:3. Obydwa korzystają z tego samego jako zmiennej wejściowej i obydwie zapiszą wynik w tym samym miejscu pamięci, co da nam jeszcze inny wynik tegoż wyścigu.

Nie będziemy analizować, który wątek (i w jakim momencie) otrzyma przydział do procesora. Potrzebujemy jedynie podstawowej wiedzy o wątkach.

Ważne!

Wielowątkowość we współczesnej informatyce jest powszechna, dzięki niej programy działają efektywniej.

W większości – jeżeli nie we wszystkich – językach programowania, które implementują wątki, występuje funkcja, która zatrzymuje wykonanie wątku nadrzędnego aż do chwili zakończenia bieżącego wątku. W językach Java, PythonC++ metoda ta nazywa się identycznie – join(). Jest ona przydatna w sytuacji, gdy dalsze operacje w programie chcemy wykonywać dopiero po zakończeniu wątku.

W programowaniu istnieją też inne rozwiązania tego problemu.

Dla zainteresowanych

Do wspomnianych rozwiązań programowych możemy zaliczyć semaforysemaforysemafory.

Wątki w języku Java

W języku Java wątki możemy stworzyć przez implementację w klasie interfejsu Runnable lub poprzez dziedziczenie po klasie Thread. Efekt działania obu rozwiązań jest taki sam, ale różnią się one znacznie pod względem sposobu kodowania. Zasadnicza różnica polega na tym, że dziedzicząc po klasie Thread, blokujemy możliwość dalszego dziedziczenia (Java nie obsługuje wielokrotnego dziedziczenia). Innymi słowy, jeżeli klasa będzie dziedziczyła po klasie Thread, to nie może już dziedziczyć po żadnej innej.

Natomiast interfejs Runnable implementujemy w konkretnej klasie, na podstawie której możemy utworzyć wiele obiektów. Nie blokujemy żadnych możliwości dziedziczenia. Dodatkowo mamy możliwość dodawania do klasy dalszych interfejsów, jeżeli zajdzie taka potrzeba. W przypadku naszego prostego przykładu nie ma jednak znaczenia, którą drogę realizacji wybierzemy.

Warto jeszcze uzupełnić, że klasa Thread jest standardową klasą reprezentującą wątek w języku Java. Udostępnia kilka ważnych i przydatnych metod oraz właściwości. Na potrzeby tego materiału poznamy tylko część najistotniejszych, niezbędnych do wykorzystania tej klasy w praktyce.

Najważniejsza w klasie Thread jest metoda run(), w niej znajduje się kod, który będzie realizowanym przez wątek. Jego działanie rozpoczyna się po wywołaniu innej należącej do klasy Thread metody, czyli start(). Krótko mówiąc wątek uruchamiamy, wywołując metodę start() i rozpoczyna on realizowanie kodu zawartego w metodzie run(). Dlatego właśnie implementując własny wątek przez dziedziczenie klasy Thread, przesłaniamy metodę run() i zapewniamy w klasie potomnej jej własną implementację.

Inną ważną metodą jest join(). Wywołując tę metodę, będziemy mogli zaimplementować oczekiwanie na zakończenie wątku. W klasie Thread znajdziemy jeszcze inne przydatne metody. Na przykład metoda sleep() pozwala na uśpienie (zatrzymanie) wątku na określony czas. W omawianym przykładzie ograniczymy się jednak do zastosowania podstawowych metod.

Sprawdźmy najpierw, czy rzeczywiście zjawisko wyścigów funkcjonuje w języku Java. Do tego posłuży kod, który w linijkach 5–9 tworzy nowe obiekty klasy Thread za pomocą konstruktora LicznikThread.

Linia 1. public class Main otwórz nawias klamrowy. Linia 2. public static void main otwórz nawias okrągły String otwórz nawias kwadratowy zamknij nawias kwadratowy args zamknij nawias okrągły throws InterruptedException otwórz nawias klamrowy. Linia 3. Licznik licznik znak równości new Licznik otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 4. int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 10 średnik. Linia 5. Thread otwórz nawias kwadratowy zamknij nawias kwadratowy watek znak równości new Thread otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 6. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości new LicznikThread otwórz nawias okrągły licznik zamknij nawias okrągły średnik. Linia 8. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 11. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 12. zamknij nawias klamrowy. Linia 13. System kropka out kropka println otwórz nawias okrągły licznik kropka wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 14. zamknij nawias klamrowy. Linia 15. zamknij nawias klamrowy.

Możemy tak zrobić, ponieważ klasa LicznikThread dziedziczy po klasie Thread.

Linia 1. class LicznikThread extends Thread otwórz nawias klamrowy. Linia 2. private Licznik licznik średnik. Linia 4. public LicznikThread otwórz nawias okrągły Licznik counter zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. this kropka licznik znak równości counter średnik. Linia 6. zamknij nawias klamrowy. Linia 8. public void run otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 9. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny 100000 średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 10. licznik kropka zwieksz otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 11. zamknij nawias klamrowy. Linia 12. zamknij nawias klamrowy. Linia 13. zamknij nawias klamrowy.

Metoda run() domyślnie występuje w klasie Thread i w linijce ósmej ją nadpisujemy (przesłaniamy). Jak już wspomnieliśmy wyżej, metoda run() zawiera kod, który będzie wykonywany w ramach wątku.

Metoda start() służy do uruchamiania wątku, a metodę join() wykorzystamy do tego, aby uzyskać pewność, że wszystkie wystartowane wątki się zatrzymały, zanim zaczniemy dalej wykonywać kod. Po wywołaniu metoda join() kończy swoje działanie (nie zwracając żadnej wartości) w momencie, gdy wątek, dla którego obiektu ją wywołaliśmy, zakończył pracę.

Linia 1. class Licznik otwórz nawias klamrowy. Linia 2. private int c znak równości 0 średnik. Linia 3. public void zwieksz otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 4. c plus plus średnik. Linia 5. zamknij nawias klamrowy. Linia 6. public int wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. return c średnik. Linia 8. zamknij nawias klamrowy. Linia 9. zamknij nawias klamrowy.

Klasa licznik zawiera w sobie wartość c, którą zmienia metoda zwieksz(). To właśnie w tej metodzie dochodzi do wyścigu.

Oto cały kod, który należy uruchomić na własnym komputerze kilka razy i sprawdzić, czy wyniki rzeczywiście są różne.

Linia 1. public class Main otwórz nawias klamrowy. Linia 2. public static void main otwórz nawias okrągły String otwórz nawias kwadratowy zamknij nawias kwadratowy args zamknij nawias okrągły throws InterruptedException otwórz nawias klamrowy. Linia 3. Licznik licznik znak równości new Licznik otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 4. int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 10 średnik. Linia 5. Thread otwórz nawias kwadratowy zamknij nawias kwadratowy watek znak równości new Thread otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 6. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości new LicznikThread otwórz nawias okrągły licznik zamknij nawias okrągły średnik. Linia 8. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 11. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 12. zamknij nawias klamrowy. Linia 13. System kropka out kropka println otwórz nawias okrągły licznik kropka wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 14. zamknij nawias klamrowy. Linia 15. zamknij nawias klamrowy. Linia 16. class LicznikThread extends Thread otwórz nawias klamrowy. Linia 17. private Licznik licznik średnik. Linia 19. public LicznikThread otwórz nawias okrągły Licznik counter zamknij nawias okrągły otwórz nawias klamrowy. Linia 20. this kropka licznik znak równości counter średnik. Linia 21. zamknij nawias klamrowy. Linia 23. public void run otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 24. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny 100000 średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 25. licznik kropka zwieksz otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 26. zamknij nawias klamrowy. Linia 27. zamknij nawias klamrowy. Linia 28. zamknij nawias klamrowy. Linia 29. class Licznik otwórz nawias klamrowy. Linia 30. private int c znak równości 0 średnik. Linia 31. public void zwieksz otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 32. c plus plus średnik. Linia 33. zamknij nawias klamrowy. Linia 34. public int wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 35. return c średnik. Linia 36. zamknij nawias klamrowy. Linia 37. zamknij nawias klamrowy.

Jak już wspomniano, jednym z ograniczeń języka Java jest to, że może on dziedziczyć jedynie po jednej klasie jednocześnie, dlatego też dziedziczenie po Thread w klasie LicznikThread jest może i dobrym pomysłem w ramach omawianego prostego przykładu, ale w ten sposób bardzo mocno ograniczamy możliwości projektowania klas.

Z tego powodu kod lepiej napisać, wykorzystując drugą metodę, tj. interfejs Runnable(). Dzięki temu nie zablokujemy możliwości dziedziczenia. Kod wówczas będzie wyglądał następująco:

Linia 1. public class Main otwórz nawias klamrowy. Linia 2. public static void main otwórz nawias okrągły String otwórz nawias kwadratowy zamknij nawias kwadratowy args zamknij nawias okrągły throws InterruptedException otwórz nawias klamrowy. Linia 3. Licznik licznik znak równości new Licznik otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 4. int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 10 średnik. Linia 5. Thread otwórz nawias kwadratowy zamknij nawias kwadratowy watek znak równości new Thread otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 6. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości new Thread otwórz nawias okrągły new LicznikRunnable otwórz nawias okrągły licznik zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 8. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 11. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 12. zamknij nawias klamrowy. Linia 13. System kropka out kropka println otwórz nawias okrągły licznik kropka wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 14. zamknij nawias klamrowy. Linia 15. zamknij nawias klamrowy. Linia 16. class LicznikRunnable implements Runnable otwórz nawias klamrowy. Linia 17. private Licznk licznik średnik. Linia 19. public LicznikRunnable otwórz nawias okrągły Licznik counter zamknij nawias okrągły otwórz nawias klamrowy. Linia 20. this kropka licznik znak równości counter średnik. Linia 21. zamknij nawias klamrowy. Linia 23. public void run otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 24. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny 100000 średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 25. licznik kropka zwieksz otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 26. zamknij nawias klamrowy. Linia 27. zamknij nawias klamrowy. Linia 28. zamknij nawias klamrowy. Linia 30. class Licznik otwórz nawias klamrowy. Linia 31. private int c znak równości 0 średnik. Linia 32. public void zwieksz otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 33. c plus plus średnik. Linia 34. zamknij nawias klamrowy. Linia 35. public int wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 36. return c średnik. Linia 37. zamknij nawias klamrowy. Linia 38. zamknij nawias klamrowy.

Korzystając z interfejsu Runnable, również obowiązkowo przesłaniamy (nadpisujemy) metodę run(), w której zawarty jest kod wykonywany w ramach wątku. Jest on stworzony z myślą o klasie Thread. Klasa Thread w konstruktorze przyjmuje obiekt klasy implementującej interfejs Runnable i zawierającej w sobie nadpisaną (realizującą wymagane przez nas zadania) metodę run().

Dla zainteresowanych

Spróbuj samodzielnie porównać działanie klasy implementującej interfejs Runnable i takiej rozszerzającej klasę Threads. Uruchom kilka razy każdy z programów i sprawdź, czy zjawisko wyścigu rzeczywiście zachodzi.

Żeby uniknąć wyścigu, możemy w języku Java użyć słowa kluczowego Synchronized, dzięki któremu dana metoda będzie wykonywana tylko przez jeden wątek w określonym momencie.

Linia 1. class Licznik otwórz nawias klamrowy. Linia 2. private int c znak równości 0 średnik. Linia 3. public synchronized void zwieksz otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 4. c plus plus średnik. Linia 5. zamknij nawias klamrowy. Linia 6. public int wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. return c średnik. Linia 8. zamknij nawias klamrowy. Linia 9. zamknij nawias klamrowy.

Po zmodyfikowaniu klasy Licznik, przez dodanie słowa kluczowego Synchronized, wynikiem programu jest liczba 1000000 za każdym uruchomieniem.

W dalszej części e‑materiału wyjaśnione zostanie działanie tego słowa kluczowego.

Mechanizm semaforów

Alternatywnie możemy skorzystać z mechanizmów semaforów lub muteksów. Wybieramy te pierwsze. Różnica między nimi jest taka, że semafory mogą być podnoszone przez dowolny wątek, natomiast muteksy jedynie przez te, które je zamknęły. Jeśli wątek próbuje przejść przez zamknięty semafor, to czeka, aż ten się otworzy. Jeśli wiele wątków czeka na otwarcie semafora, to wtedy tylko jeden z nich, losowy, przejdzie przez niego, jednocześnie go opuszczając. Pozostałe będą dalej czekać.

Obiekt klasy Semaphore tworzymy w następujący sposób:

Linia 1. Semaphore semafor znak równości new Semaphore otwórz nawias okrągły 1 zamknij nawias okrągły średnik.

W konstruktorze jako argument podajemy, ile wątków może przez niego przejść, zanim ten się definitywnie zamknie. W omawianym wypadku będzie to właśnie 1.

Próba zajęcia semafora przez wątek może wywołać wyjątek InterruptedException, więc użyjemy try/catch, żeby go obsłużyć.

Metoda acquire() z klasy Semaphore służy do opuszczania, a w zasadzie do zmniejszania liczby „zgód” na wejście wątku w dane miejsce. Jeśli liczba ta wyniesie 0, to semafor uznajemy za zamknięty.

Metoda release() z kolei podnosi liczbę „zgód” na wejście wątków w daną sekcję krytyczną.

Linia 1. class Licznik otwórz nawias klamrowy. Linia 2. private int c znak równości 0 średnik. Linia 3. Semaphore semafor znak równości new Semaphore otwórz nawias okrągły 1 zamknij nawias okrągły średnik. Linia 4. public void zwieksz otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. try otwórz nawias klamrowy. Linia 6. semafor kropka acquire otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 7. zamknij nawias klamrowy catch otwórz nawias okrągły InterruptedException e zamknij nawias okrągły otwórz nawias klamrowy. Linia 8. e kropka printStackTrace otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. c plus plus średnik. Linia 11. semafor kropka release otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 12. zamknij nawias klamrowy. Linia 13. public int wyswietlWartosc otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 14. return c średnik. Linia 15. zamknij nawias klamrowy. Linia 16. zamknij nawias klamrowy.

Pokazaliśmy więc, jak wyglądałaby klasa Licznik, gdybyśmy do synchronizacji użyli semaforów.

Monitor na obiektach

W języku Java istnieje wiele możliwości synchronizacji wątków. Podczas omawiania tego tematu należy koniecznie wspomnieć, że każdy tworzony obiekt dziedziczy swoje właściwości po klasie Object(), w której są zawarte, m.in. takie metody jak:
wait(), notify() oraz notifyAll().

Każdy obiekt w języku Java posiada wbudowany w siebie monitor, inaczej mówiąc zamek, który kontrolowany jest za pomocą wyżej wymienionych funkcji.

Pierwsza ze wspomnianych metod zatrzymuje pracę wątku do czasu otrzymania przez obiekt (zamek), podawanego jako argument, stosownego komunikatu.

Druga ze wspomnianych metod wysyła pojedynczy komunikat przechwytywany przez jeden czekający wątek, który oczekuje na odblokowanie konkretnego zablokowanego obiektu (zamka). Jeżeli żaden wątek z danym zamkiem nie czeka, to operacja ta nie ma w praktyce żadnych efektów.

Trzecia ze wspomnianych metod wysyła komunikat do wszystkich wątków czekających na odblokowanie zamka danego obiektu. Podobnie jak w przypadku notify(), jeżeli żaden wątek nie czeka na odblokowanie tego zamka, to operacja ta nie ma efektu.

W momencie używania słowa kluczowego synchronized() w definicji metody, wspomniany monitor też jest używany. Warto tutaj dodać, że sformułowanie takie jak:

Linia 1. void synchronized przyklad otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. zamknij nawias klamrowy.

Działałby identycznie jak:

Linia 1. void przyklad otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. synchronized otwórz nawias okrągły this zamknij nawias okrągły otwórz nawias klamrowy. Linia 4. zamknij nawias klamrowy. Linia 5. zamknij nawias klamrowy.

Aby skorzystać z wyżej wymienionych metod, należy użyć ich w ramach bloku synchronized() i wskazać obiekt, na którym chcemy założyć wspominany zamek.

Czyli żeby skorzystać z metody wait(), w ramach przykładowej metody zrealizowalibyśmy to w następujący sposób:

Linia 1. void przyklad otwórz nawias okrągły Object zamek zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. synchronized otwórz nawias okrągły zamek zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. zamek kropka wait otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 4. zamknij nawias klamrowy. Linia 5. zamknij nawias klamrowy.

Wtedy też moglibyśmy użyć obiektu zamek klasy Object w sposób podobny do semaforów. Różnicą pomiędzy tymi dwoma rozwiązaniami jest to, że metoda notify() nie zapisuje „zgód” na zapas, to znaczy możliwe jest, że metoda ta nie spowoduje, że uruchomi się jakiś wątek, ponieważ żaden wątek nie będzie oczekiwał na ten komunikat. Dlatego też warunkiem efektywności metody notify(), jest to, żeby w czasie jej wykonywania jakiś wątek oczekiwał na komunikat przez nią wysyłany.

Przykład realizujący zadanie analogiczne do powyższego wyglądałby następująco:

Linia 1. public class Main otwórz nawias klamrowy. Linia 2. public static void main otwórz nawias okrągły String otwórz nawias kwadratowy zamknij nawias kwadratowy args zamknij nawias okrągły throws InterruptedException otwórz nawias klamrowy. Linia 4. int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 50 średnik prawy ukośnik prawy ukośnik liczba uruchamianych 50 wątków kropka. Linia 5. Thread otwórz nawias kwadratowy zamknij nawias kwadratowy watek znak równości new Thread otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 6. LicznikWrapper obiektZawierajacyLicznik znak równości new LicznikWrapper otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 7. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 8. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości new Thread otwórz nawias okrągły new Wyswietlacz otwórz nawias okrągły i przecinek obiektZawierajacyLicznik zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 9. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 10. zamknij nawias klamrowy. Linia 12. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny 1000 średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 13. synchronized otwórz nawias okrągły obiektZawierajacyLicznik zamknij nawias okrągły otwórz nawias klamrowy. Linia 14. obiektZawierajacyLicznik kropka notify otwórz nawias okrągły zamknij nawias okrągły średnik prawy ukośnik prawy ukośnik wznawiamy wątek używający jako zamka obiektu zawierającego licznik. Linia 15. prawy ukośnik prawy ukośnik o ile jakiś na to oczekuje. Linia 16. zamknij nawias klamrowy. Linia 17. zamknij nawias klamrowy. Linia 18. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły otwórz nawias klamrowy. Linia 19. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka interrupt otwórz nawias okrągły zamknij nawias okrągły średnik prawy ukośnik prawy ukośnik informujemy wątek przecinek że już nie musi pracować. Linia 20. watek otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 21. zamknij nawias klamrowy. Linia 22. System kropka out kropka println otwórz nawias okrągły cudzysłów Ostateczna wartosc obiektu licznik klasy Integer to dwukropek cudzysłów plus obiektZawierajacyLicznik kropka licznik zamknij nawias okrągły średnik. Linia 23. zamknij nawias klamrowy. Linia 24. zamknij nawias klamrowy. Linia 26. class LicznikWrapper. Linia 27. otwórz nawias klamrowy. Linia 28. int licznik znak równości 0 średnik. Linia 29. zamknij nawias klamrowy. Linia 31. class Wyswietlacz implements Runnable. Linia 32. otwórz nawias klamrowy. Linia 33. int numerWatku znak równości minus 1 średnik. Linia 34. final LicznikWrapper obiektZawierajacyLicznik średnik. Linia 35. Wyswietlacz otwórz nawias okrągły int numerWatku przecinek LicznikWrapper obiektZawierajacyLicznik zamknij nawias okrągły otwórz nawias klamrowy. Linia 36. this kropka numerWatku znak równości numerWatku średnik. Linia 37. this kropka obiektZawierajacyLicznik znak równości obiektZawierajacyLicznik średnik. Linia 38. zamknij nawias klamrowy. Linia 39. at Override. Linia 40. public void run otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 41. while otwórz nawias okrągły true zamknij nawias okrągły otwórz nawias klamrowy. Linia 42. synchronized otwórz nawias okrągły obiektZawierajacyLicznik zamknij nawias okrągły. Linia 43. otwórz nawias klamrowy. Linia 44. try otwórz nawias klamrowy. Linia 45. obiektZawierajacyLicznik kropka wait otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 46. obiektZawierajacyLicznik kropka licznik plus plus średnik. Linia 47. System kropka out kropka println otwórz nawias okrągły cudzysłów Watek numer cudzysłów plus numerWatku plus cudzysłów a licznik to cudzysłów plus obiektZawierajacyLicznik kropka licznik zamknij nawias okrągły średnik. Linia 48. zamknij nawias klamrowy catch otwórz nawias okrągły InterruptedException e zamknij nawias okrągły otwórz nawias klamrowy. Linia 49. break średnik prawy ukośnik prawy ukośnik Koniecznie potrzebne przecinek ponieważ robimy ręcznie. Linia 50. prawy ukośnik prawy ukośnik interrupt na wszystkich wątkach przecinek by te zakończyły swoją pracę po 1000 wykonań kropka. Linia 51. zamknij nawias klamrowy. Linia 52. zamknij nawias klamrowy. Linia 53. zamknij nawias klamrowy. Linia 54. zamknij nawias klamrowy. Linia 55. zamknij nawias klamrowy.
Dla zainteresowanych

Przetestuj pracę programu dla mniejszej liczby wątków, a następnie porównaj działanie zamku tworzonego na obiekcie do działania semafora.

Podkreślmy, że niezapisywanie przez notify() nadmiarowych „zgód” (czy w tym przypadku komunikatów), wcale nie jest wadą tego mechanizmu, tylko jego cechą. Przykładowo program, w którym realizowalibyśmy symulację wsiadania pasażerów do pociągu, przy pewnych założeniach, mógłby być prostszy do zaimplementowania z użyciem metod notifyAll()wait(), niż semaforów. W takim programie wątki byłyby pasażerami, czekającymi na wjazd pociągu na peron. Oznacza to, że każdy z obiektów klasy pasażer byłby osobnym wątkiem, wykonującym – na obiekcie klasy peron – metodę wait(). Z kolei pociąg wykonałby metodę notifyAll() (na wspominanym wcześniej peronie), żeby powiadomić wszystkich pasażerów, że już nadjechał i można do niego wsiadać. Następnie pasażerowie wchodziliby do pociągu do momentu, gdy skończyłoby się w nim miejsca lub nie byłoby już więcej oczekujących pasażerów. Wtedy pociąg odjeżdżałby, a kolejni pasażerowie, którzy dopiero przybyliby na peron, musieliby oczekiwać na kolejny pociąg.

Dlatego też mechanizmów synchronizacji należy używać zależnie od potrzeby.

Dla zainteresowanych

Do dalszego zgłębiania tematu dotyczących synchronizacji wątków w języku Java zachęcamy samodzielnie.

Współbieżność w C++

W języku C++ współbieżność można zaimplementować na kilka sposobów. Zrobimy to z pomocą biblioteki thread, dołączonej do języka C++11. Podstawową klasą obsługującą wątki jest w tej bibliotece klasa thread. Obiekt tej klasy reprezentuje pojedynczy wątek.

Istnieje analogiczna biblioteka, która wywodzi się jeszcze z języka C, czyli pthread, której jednak nie będziemy omawiać.

Konstruktor klasy thread jako pierwszy parametr przyjmuje wskaźnik do funkcji, która będzie realizowana w ramach wątku, a kolejne argumenty konstruktora to po prostu argumenty tej funkcji. Zamiast funkcji można użyć również klasy, co wyjaśnimy  dalej.

Poniżej możemy zobaczyć przykład najprostszego programu wykorzystującego wątki. W ramach każdego wątku wykonywana jest funkcja zwieksz(), która jako argument przyjmuje wskaźnik do liczby, którą potem zwiększa o n. Konstruktor wątku przyjmuje jako argument jej nazwę. W przykładzie uruchamianych jest n wątków, których obiekty przechowywane są w tablicy th[].

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. kratka include otwórz nawias ostrokątny thread zamknij nawias ostrokątny. Linia 3. using namespace std średnik. Linia 4. const int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 5000 średnik. Linia 5. void zwieksz otwórz nawias okrągły int asterysk c zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 7. otwórz nawias klamrowy. Linia 8. otwórz nawias okrągły asterysk c zamknij nawias okrągły plus plus średnik. Linia 9. zamknij nawias klamrowy. Linia 10. zamknij nawias klamrowy. Linia 11. int main otwórz nawias okrągły zamknij nawias okrągły. Linia 12. otwórz nawias klamrowy. Linia 13. int c znak równości 0 średnik. Linia 14. thread th otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 16. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 17. otwórz nawias klamrowy. Linia 18. th otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości thread otwórz nawias okrągły zwieksz przecinek ampersant c zamknij nawias okrągły średnik. Linia 19. zamknij nawias klamrowy. Linia 20. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 21. otwórz nawias klamrowy. Linia 22. th otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 23. zamknij nawias klamrowy. Linia 24. cout otwórz nawias ostrokątny otwórz nawias ostrokątny c średnik. Linia 25. zamknij nawias klamrowy.

Obiekt klasy thread od razu po utworzeniu włącza się i zaczyna pracę, nie ma więc potrzeby pobudzania go. Natomiast funkcja join() klasy thread pozwala na zatrzymanie pracy głównego programu aż do chwili ukończenia działania wątku. Nie zwraca ona żadnej wartości, ale kończy działanie wraz z zakończeniem pracy wątku.

Przykład ten pokazuje możliwość przekazania do wątku klasy zamiast funkcji. W takim przypadku pierwszym argumentem konstruktora obiektu thread powinien być operator() danej klasy. Przykład realizuje to samo proste zadanie, co kod powyżej, tyle że zwiększanie liczby o n dokonywane jest w klasie Przyklad, zamiast w funkcji zwieksz().

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. kratka include otwórz nawias ostrokątny thread zamknij nawias ostrokątny. Linia 3. using namespace std średnik. Linia 4. const int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 5000 średnik. Linia 6. class Przyklad otwórz nawias klamrowy. Linia 7. public dwukropek. Linia 8. void operator otwórz nawias okrągły zamknij nawias okrągły otwórz nawias okrągły int asterysk c zamknij nawias okrągły. Linia 9. otwórz nawias klamrowy. Linia 10. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 11. otwórz nawias klamrowy. Linia 12. otwórz nawias okrągły asterysk c zamknij nawias okrągły plus plus średnik. Linia 13. zamknij nawias klamrowy. Linia 14. zamknij nawias klamrowy. Linia 15. zamknij nawias klamrowy średnik. Linia 17. int main otwórz nawias okrągły zamknij nawias okrągły. Linia 18. otwórz nawias klamrowy. Linia 19. int c znak równości 0 średnik. Linia 20. thread th otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 21. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 22. otwórz nawias klamrowy. Linia 23. th otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości thread otwórz nawias okrągły Przyklad otwórz nawias okrągły zamknij nawias okrągły przecinek ampersant c zamknij nawias okrągły średnik. Linia 24. zamknij nawias klamrowy. Linia 25. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 26. otwórz nawias klamrowy. Linia 27. th otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 28. zamknij nawias klamrowy. Linia 29. cout otwórz nawias ostrokątny otwórz nawias ostrokątny c średnik. Linia 30. zamknij nawias klamrowy.

W kolejnym przykładzie zobaczymy, w jaki sposób do konstruktora klasy thread można przekazać więcej zmiennych. Kod realizuje podobne zadanie jak poprzednie, przy czym tym razem w klasie Przyklad przeliczane są trzy zmienne: pierwsza jest zwiększana n razy, druga jest zmniejszana n razy, a trzecia – jest n razy powiększana o 2. Do konstruktora obiektu wątku przekazywane są więc trzy wskaźniki do zmiennych.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. kratka include otwórz nawias ostrokątny thread zamknij nawias ostrokątny. Linia 3. using namespace std średnik. Linia 5. const int otwórz nawias ostrokątny code style znak równości cudzysłów white minus space dwukropek pre średnik cudzysłów data minus inline zamknij nawias ostrokątny n otwórz nawias ostrokątny prawy ukośnik code zamknij nawias ostrokątny znak równości 5000 średnik. Linia 7. class Przyklad otwórz nawias klamrowy. Linia 8. public dwukropek. Linia 9. void operator otwórz nawias okrągły zamknij nawias okrągły otwórz nawias okrągły int asterysk c przecinek int asterysk d przecinek int asterysk f zamknij nawias okrągły. Linia 10. otwórz nawias klamrowy. Linia 11. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 12. otwórz nawias klamrowy. Linia 13. otwórz nawias okrągły asterysk c zamknij nawias okrągły plus plus średnik. Linia 14. otwórz nawias okrągły asterysk d zamknij nawias okrągły minus minus średnik. Linia 15. asterysk f plus znak równości 2 średnik. Linia 16. zamknij nawias klamrowy. Linia 17. zamknij nawias klamrowy. Linia 18. zamknij nawias klamrowy średnik. Linia 20. int main otwórz nawias okrągły zamknij nawias okrągły. Linia 21. otwórz nawias klamrowy. Linia 22. int c znak równości 0 średnik. Linia 23. int d znak równości 0 średnik. Linia 24. int f znak równości 0 średnik. Linia 25. thread th otwórz nawias kwadratowy n zamknij nawias kwadratowy średnik. Linia 26. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 27. otwórz nawias klamrowy. Linia 28. th otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości thread otwórz nawias okrągły Przyklad otwórz nawias okrągły zamknij nawias okrągły przecinek ampersant c przecinek ampersant d przecinek ampersant f zamknij nawias okrągły średnik. Linia 29. zamknij nawias klamrowy. Linia 30. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 31. otwórz nawias klamrowy. Linia 32. th otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 33. zamknij nawias klamrowy. Linia 34. cout otwórz nawias ostrokątny otwórz nawias ostrokątny c otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 35. cout otwórz nawias ostrokątny otwórz nawias ostrokątny d otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 36. cout otwórz nawias ostrokątny otwórz nawias ostrokątny f otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 37. zamknij nawias klamrowy.
Dla zainteresowanych

Sprawdź, jak działa przedstawiony program na twoim komputerze. Uruchom go kilka razy i porównaj, czy wyniki się od siebie różnią. Jeśli za każdym razem są identyczne, spróbuj zwiększyć zmienną n, która odpowiada za liczbę powoływanych wątków i za długość pętli.

W tej bibliotece nie istnieje wbudowana możliwość automatycznego synchronizowania metod jak w języku Java. Nie oznacza to jednak, że nie można synchronizować w niej wątków. Można zrobić to w sposób programowy czy choćby przez stosowanie muteksów lub semaforów.

Muteksy i semafory są podobne, ale nie można używać ich wymiennie.

Mechanizmy muteksów

Użyjemy muteksów, które charakteryzują się tym, że tylko jeden wątek może znajdować się w sekcji krytycznejsekcja krytycznasekcji krytycznej. Warto dodać, że każdy mutex musi być opuszczony i podniesiony przez ten sam wątek i niemożliwa jest sytuacja, w której dwa wątki będą operować w sekcji krytycznej ograniczonej właśnie przez mutex.

Muteksy są dostępne w bibliotece <mutex>, będącej w standardzie od C++11.

Warto dodać, że semafory zostały wprowadzone po raz pierwszy w standardzie C++20.

Linia 1. mutex zamek średnik. Linia 2. class Przyklad otwórz nawias klamrowy. Linia 3. public dwukropek. Linia 4. void operator otwórz nawias okrągły zamknij nawias okrągły otwórz nawias okrągły int asterysk c przecinek int asterysk d przecinek int asterysk f zamknij nawias okrągły. Linia 5. otwórz nawias klamrowy. Linia 6. zamek kropka lock otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 7. for otwórz nawias okrągły int i znak równości 0 średnik i otwórz nawias ostrokątny n średnik i plus plus zamknij nawias okrągły. Linia 8. otwórz nawias klamrowy. Linia 9. otwórz nawias okrągły asterysk c zamknij nawias okrągły plus plus średnik. Linia 10. otwórz nawias okrągły asterysk d zamknij nawias okrągły minus minus średnik. Linia 11. asterysk f plus znak równości 2 średnik. Linia 12. zamknij nawias klamrowy. Linia 13. zamek kropka unlock otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 14. zamknij nawias klamrowy. Linia 15. zamknij nawias klamrowy średnik.

Jeśli zapiszemy kod w ten sposób, otrzymamy następujący wydruk:

Linia 1. 25000000. Linia 2. minus 25000000. Linia 3. 50000000.

Dzieje się tak, ponieważ dzięki naszemu zamkowi tylko jeden wątek na raz może znajdować się w sekcji krytycznej.

Dla zainteresowanych

Spróbuj teraz uruchomić kod, włącznie z przedstawioną edycją i zobacz, czy rzeczywiście wyniki będą przewidywalne. Spróbuj z innymi liczbami przejść pętle i wątki.

Współbieżność w języku Python

W języku Python za współbieżność odpowiada klasa Thread, dostępna w bibliotece  threading. Klasa ta ma w swoim konstruktorze dwa argumenty, które nas interesują. Pierwszym z nich jest argument target, za pomocą którego wskazujemy funkcję lub metodę, w której dany wątek rozpocznie pracę. Drugim jest argument args(), w którym wpisujemy argumenty funkcji wskazanej w pierwszym argumencie. Argumenty wpisujemy w tej samej kolejności, w jakiej pojawiają się w definicji funkcji lub metody.

Za pomocą kodu możemy zasymulować zjawisko wyścigu:

Linia 1. import threading. Linia 2. x znak równości 0 średnik. Linia 3. def przyklad otwórz nawias okrągły zamknij nawias okrągły dwukropek. Linia 4. zwieksz otwórz nawias okrągły zamknij nawias okrągły. Linia 5. return. Linia 6. def zwieksz otwórz nawias okrągły zamknij nawias okrągły dwukropek. Linia 7. for podkreślnik in range otwórz nawias okrągły 100000 zamknij nawias okrągły dwukropek. Linia 8. global x średnik. Linia 9. x znak równości x plus 2 średnik. Linia 10. threads znak równości otwórz nawias kwadratowy zamknij nawias kwadratowy. Linia 11. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 12. t znak równości threading kropka Thread otwórz nawias okrągły target znak równości przyklad zamknij nawias okrągły. Linia 13. threads kropka append otwórz nawias okrągły t zamknij nawias okrągły. Linia 14. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 15. threads otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 16. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 17. threads otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 18. print otwórz nawias okrągły x zamknij nawias okrągły średnik.

W 12. wierszu rozpoczynamy działanie nowych wątków, które za argument przyjmują funkcję przyklad(). Następnie wszystkie są uruchamiane metodą start() w 15. wierszu. Ważne, żeby uruchomić stworzone wątki – jeśli tego nie zrobimy, nie zaczną pracy. Ostatnią metodą z klasy Thread, którą omówimy, będzie join(). Metoda ta służy do upewniania się, że – do tego momentu – wątek zakończył swoje działanie. Jeśli dany wątek nie zakończył działania, cały program czeka, aż ten się zakończy. Jeżeli nie będziemy używać tej metody, program może zakończyć swoje działanie zbyt wcześnie - przed zakończeniem wszystkich wątków. W naszym przykładzie bez linijek 16–17 istnieje szansa, że linijka 18 wykona się, zanim wszystkie wątki zakończą działanie, co z kolei wypisze nam zły wynik.

Jak zapobiec wyścigowi?

Wyścig, zgodnie z tym, co zostało opisane wcześniej, jest zjawiskiem nieprzewidywalnym z perspektywy programisty. Aby mu zapobiec, skorzystamy z threading.Lock(), czyli z klasy dostępnej z biblioteki threading.

Możemy skorzystać również z semaforówsemaforysemaforów, czy innych form zarządzania sekcjami krytycznymisekcja krytycznasekcjami krytycznymi, jednakże w tym e‑materiale ograniczymy się tylko do zamków.

W języku Python klasa threading.Lock() może być otwierana i zamykana przez dowolny wątek. Oznacza to, że jeden wątek może zamknąć sekcje krytyczną, a drugi ją otworzyć bez zgody poprzedniego – co w zależności od sytuacji może być pożądane lub nie.

Do zamykania używamy metody acquire(), a do otwierania metody release(). Metoda acquire() może przyjmować argumenty, które dodatkowo zmodyfikują zachowanie zamka.

Sam zamek tworzymy w następujący sposób:

Linia 1. zamek znak równości threading kropka Lock otwórz nawias okrągły zamknij nawias okrągły średnik.

Tam zamek zamykamy:

Linia 1. zamek kropka acquire otwórz nawias okrągły zamknij nawias okrągły średnik.

Tak otwieramy:

Linia 1. zamek kropka release otwórz nawias okrągły zamknij nawias okrągły średnik.

Kod z użyciem zamków mógłby wyglądać w następujący sposób:

Linia 1. import threading. Linia 2. x znak równości 0 średnik. Linia 3. zamek znak równości threading kropka Lock otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 5. def przyklad otwórz nawias okrągły zamknij nawias okrągły dwukropek. Linia 6. zwieksz otwórz nawias okrągły zamknij nawias okrągły. Linia 7. return. Linia 10. def zwieksz otwórz nawias okrągły zamknij nawias okrągły dwukropek. Linia 11. zamek kropka acquire otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 12. for podkreślnik in range otwórz nawias okrągły 100000 zamknij nawias okrągły dwukropek. Linia 13. global x średnik. Linia 14. x znak równości x plus 2 średnik. Linia 15. zamek kropka release otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 17. threads znak równości otwórz nawias kwadratowy zamknij nawias kwadratowy. Linia 18. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 19. t znak równości threading kropka Thread otwórz nawias okrągły target znak równości przyklad zamknij nawias okrągły. Linia 20. threads kropka append otwórz nawias okrągły t zamknij nawias okrągły. Linia 21. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 22. threads otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka start otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 23. for i in range otwórz nawias okrągły 1000 zamknij nawias okrągły dwukropek. Linia 24. threads otwórz nawias kwadratowy i zamknij nawias kwadratowy kropka join otwórz nawias okrągły zamknij nawias okrągły średnik. Linia 25. print otwórz nawias okrągły x zamknij nawias okrągły średnik.

Dzięki zamkowi mamy pewność, że nie dojdzie do wyścigu. Oznacza to, że za każdym uruchomieniem programu zostanie wypisane „200000000”.

Słownik

mechanizm synchronizacji
mechanizm synchronizacji

zamek, semafor, algorytm bądź inny dowolny sposób, który zapewnia synchroniczną i skoordynowaną pracę wielu wątków wykonujących te same polecenia

proces
proces

egzemplarz wykonywanego programu; aplikacja może składać się z jednego lub z większej liczby procesów; system operacyjny przydziela każdemu procesowi zasoby (takie jak pamięć, czas procesora, dostęp do plików i urządzeń wejścia/wyjścia). Każdy proces może zażądać utworzenia określonej liczby wątków, wykonujących poszczególne części programu; wątki współdzielą prawie wszystkie zasoby zarezerwowane dla danego procesu, wyjątkiem jest czas procesora, który jest przydzielany każdemu wątkowi osobno

sekcja krytyczna
sekcja krytyczna

fragment kodu korzystający z zasobu dzielonego (którym może być zmienna czy inny dowolny fragment pamięci); jeśli w sekcji krytycznej pracowałyby dwa wątki, doszłoby do wyścigu; jest ona „wąskim gardłem” dla całego programu korzystającego z wątków

semafory
semafory

semaforem jest specjalna zmienna chroniona, która pozwala na kontrolę dostępu przez wiele procesów do wspólnego zasobu (np. obiektu); typowy semafor implementowany jest jako zmienna typu całkowitego

wielowątkowość
wielowątkowość

cecha systemu operacyjnego, dzięki której w ramach jednego procesu może być wykonywanych kilka niezależnych wątków; wszystkie wątki w ramach tego samego procesu współdzielą tą samą przestrzeń adresową zawierającą kod programu i jego dane; wielowątkowość to także cecha procesorów, oznaczająca możliwość jednoczesnego wykonywania wielu wątków w sposób sprzętowy na pojedynczej jednostce wykonawczej – rdzeniu fizycznym

wyścig
wyścig

może wystąpić, gdy co najmniej dwa wątki pracują jednocześnie na tych samych komórkach pamięci; może powodować błędy odczytu danych oraz błędy w obliczeniach; w przypadku wystąpienia wyścigu wynik operacji jest zależny od dokładnego momentu jej realizacji, a ten jest niemożliwy do przewidzenia