8. Wątki wykonawcze
8.1. Wprowadzenie
Po uruchomieniu aplikacja działa w strumieniu wykonania zwanym wątkiem. Klasą modelującą thread jest klasa System.Threading.Thread, która ma następującą definicję:

Będziemy korzystać tylko z niektórych właściwości i metod tej klasy:
zwraca wątek aktualnie wykonywany | |
nazwa wątku | |
wskazuje, czy wątek jest aktywny (true), czy nie (false) | |
uruchamia wątek | |
ostatecznie zatrzymuje wykonywanie wątku | |
wstrzymuje wykonywanie wątku na n milisekund | |
tymczasowo zawiesza wykonywanie wątku | |
wznowi wykonywanie zawieszonego wątku | |
operacja blokująca – czeka na zakończenie wątku, aby przejść do następnej instrukcji |
Przyjrzyjmy się pierwszej aplikacji ilustrującej istnienie głównego wątku wykonania, w którym realizowana jest funkcja Main należąca do klasy:
' wykorzystanie wątków
Imports System
Imports System.Threading
Public Module thread1
Public Sub Main()
' inicjowanie bieżącego wątku
Dim main As Thread = Thread.CurrentThread
' wyświetlanie
Console.Out.WriteLine(("Thread courant : " + main.Name))
' zmiana nazwy
main.Name = "main"
' weryfikacja
Console.Out.WriteLine(("Thread courant : " + main.Name))
' pętla nieskończona
While True
' wyświetlanie
Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' tymczasowe zatrzymanie
Thread.Sleep(1000)
End While
End Sub
End Module
Wyniki wyświetlane na ekranie:
dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59
Powyższy przykład ilustruje następujące kwestie:
- funkcja Main działa poprawnie w wątku
- dostęp do właściwości tego wątku uzyskuje się za pomocą funkcji Thread.CurrentThread
- rolę metody Sleep. W tym przypadku wątek wykonujący funkcję Main regularnie przechodzi w stan uśpienia na 1 sekundę pomiędzy kolejnymi wyświetleniami.
8.2. Tworzenie wątków wykonawczych
Możliwe jest tworzenie aplikacji, w których fragmenty kodu są wykonywane „równocześnie” w różnych wątkach wykonawczych. Stwierdzenie, że thread są wykonywane równocześnie, jest często nadużyciem językowym. Jeśli komputer ma tylko jeden procesor, co nadal często ma miejsce, procesy thread dzielą ten procesor: każdy z nich ma do niego dostęp po kolei przez krótką chwilę (kilka milisekund). To właśnie stwarza iluzję równoległości wykonywania. Czas przydzielony procesowi thread zależy od różnych czynników, w tym od jego priorytetu, który ma wartość domyślną, ale może być również ustalony programowo. Gdy proces thread ma dostęp do procesora, zazwyczaj wykorzystuje go przez cały przydzielony mu czas. Może jednak zwolnić go przed upływem tego czasu:
- przechodząc w stan oczekiwania na zdarzenie (wait, join, suspend)
- przechodząc w stan uśpienia na określony czas (sleep)
- Wątek T jest najpierw tworzony przez jego konstruktor
ThreadStart jest typu delegate i definiuje prototyp funkcji bez parametrów:
Klasyczna konstrukcja wygląda następująco:
Funkcja run przekazana jako parametr zostanie wykonana przy uruchomieniu wątku.
- Wykonanie wątku T jest uruchamiane przez funkcję T.Start(): funkcja [run] przekazana do konstruktora T zostanie następnie wykonana przez wątek T. Program wykonujący instrukcję T.start() nie czeka na zakończenie zadania T: natychmiast przechodzi do następnej instrukcji. Mamy zatem dwa zadania wykonywane równolegle. Często muszą one mieć możliwość komunikowania się między sobą, aby wiedzieć, na jakim etapie znajduje się wspólna praca do wykonania. Jest to problem synchronizacji wątków.
- Po uruchomieniu wątek thread działa autonomicznie. Zakończy działanie, gdy funkcja start, którą wykonuje, zakończy swoją pracę.
- Do zadania T można wysyłać określone sygnały:
- T.Suspend() nakazuje mu chwilowe zatrzymanie się
- T.Resume() nakazuje mu wznowienie pracy
- T.Abort() nakazuje jej ostateczne zatrzymanie
- Można również poczekać na zakończenie jego wykonywania za pomocą T.join(). Jest to instrukcja blokująca: program, który ją wykonuje, pozostaje zablokowany do momentu, aż zadanie T zakończy swoją pracę. Jest to sposób na synchronizację.
Przyjrzyjmy się następującemu programowi:
' opcje
Option Strict On
Option Explicit On
' przestrzenie nazw
Imports System
Imports System.Threading
Module thread2
Public Sub Main()
' inicjowanie bieżącego wątku
Dim main As Thread = Thread.CurrentThread
' nadanie nazwy wątku
main.Name = "main"
' tworzenie wątków wykonawczych
Dim tâches(4) As Thread
Dim i As Integer
For i = 0 To tâches.Length - 1
' tworzenie wątku i
tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
' ustalanie nazwy wątku
tâches(i).Name = "tache_" & i
' uruchamianie wątku i
tâches(i).Start()
Next i
' koniec procedury głównej
Console.Out.WriteLine(("fin du thread " + main.Name))
End Sub
Public Sub affiche()
' wyświetlenie informacji o rozpoczęciu wykonywania
Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' przejście w stan uśpienia na 1 s
Thread.Sleep(1000)
' wyświetlenie zakończenia wykonywania
Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
End Sub
End Module
Wątek główny, który wykonuje funkcję Main, tworzy 5 innych wątków odpowiedzialnych za wykonanie metody statycznej affiche. Wyniki są następujące:
dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54
Wyniki te są bardzo pouczające:
- przede wszystkim widać, że uruchomienie wątku nie powoduje blokady. Metoda Main uruchomiła równolegle 5 wątków i zakończyła swoje działanie przed nimi. Operacja
uruchamia wątek tâches[i], ale po wykonaniu tej czynności realizacja kodu przechodzi natychmiast do kolejnej instrukcji, nie czekając na zakończenie działania tego wątku.
- Wszystkie utworzone wątki muszą wykonać metodę affiche. Kolejność wykonywania jest nieprzewidywalna. Nawet jeśli w tym przykładzie kolejność wykonywania wydaje się odpowiadać kolejności żądań wykonania, nie można z tego wyciągać ogólnych wniosków. System operacyjny dysponuje tutaj 6 wątkami i jednym procesorem. Rozdzieli on zasoby procesora między te 6 wątków zgodnie z własnymi regułami.
- W wynikach widać efekt działania metody Sleep. W tym przykładzie to wątek 0 jako pierwszy wykonuje metodę affiche. Wyświetlany jest komunikat o rozpoczęciu wykonywania, a następnie wątek ten wykonuje metodę Sleep, która zawiesza go na 1 sekundę. W ten sposób traci on dostęp do procesora, który staje się dostępny dla innego wątku. Przykład pokazuje, że procesor ten przejmie wątek 1. Wątek 1 będzie podążał tą samą ścieżką, podobnie jak pozostałe wątki. Gdy upłynie sekunda zawieszenia wątku 0, jego wykonywanie może zostać wznowione. System przydziela mu procesor i może on zakończyć wykonywanie metody affiche.
Zmodyfikujmy nasz program, aby zakończyć metodę Main za pomocą instrukcji:
Wynik wykonania nowego programu wygląda następująco:
Wątki utworzone przez funkcję Main nie są wykonywane. Jest to instrukcja
powoduje to: usuwa ona wszystkie wątki aplikacji, a nie tylko wątek Main. Rozwiązaniem tego problemu jest to, aby metoda Main czekała na zakończenie działania wątków, które utworzyła, zanim sama się zakończy. Można to osiągnąć za pomocą metody Join klasy Thread:
' oczekiwanie na zakończenie wykonywania wszystkich wątków
For i = 0 To tâches.Length - 1
' oczekiwanie na zakończenie wykonywania wątku i
tâches(i).Join()
Next i 'for
' koniec procedury głównej
Console.Out.WriteLine(("fin du thread " + main.Name))
Environment.Exit(0)
Otrzymujemy wówczas następujące wyniki:
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main
8.3. Znaczenie wątków
Skoro już wykazaliśmy istnienie domyślnego wątku, który wykonuje metodę Main, i wiemy, jak tworzyć kolejne, zastanówmy się, jakie korzyści płyną z wątków i dlaczego je tutaj omawiamy. Istnieje pewien rodzaj aplikacji, które dobrze nadają się do wykorzystania wątków – są to aplikacje typu klient-serwer w Internecie. W takiej aplikacji serwer znajdujący się na komputerze S1 odpowiada na żądania klientów znajdujących się na zdalnych komputerach C1, C2, ..., Cn.
![]() |
Codziennie korzystamy z aplikacji internetowych zgodnych z tym schematem: serwisy internetowe, poczta elektroniczna, przeglądanie forów, przesyłanie plików... W powyższym schemacie serwer S1 musi obsługiwać klientów Ci jednocześnie. Jeśli weźmiemy za przykład serwer FTP (File Transfer Protocol), który dostarcza pliki swoim klientom, wiemy, że transfer pliku może czasami trwać kilka godzin. Oczywiście nie wchodzi w grę, aby jeden klient samodzielnie monopolizował serwer przez tak długi czas. Zazwyczaj serwer tworzy tyle wątków wykonawczych, ile jest klientów. Każdy wątek jest wówczas odpowiedzialny za obsługę konkretnego klienta. Ponieważ procesor jest cyklicznie dzielony między wszystkie aktywne wątki na maszynie, serwer poświęca nieco czasu każdemu klientowi, zapewniając w ten sposób równoczesność obsługi.
![]() |
8.4. Dostęp do zasobów współdzielonych
W przytoczonym powyżej przykładzie relacji klient-serwer każdy wątek obsługuje klienta w sposób w dużej mierze niezależny. Niemniej jednak wątki mogą być zmuszone do współpracy w celu zrealizowania usługi żądanej przez ich klienta, zwłaszcza w przypadku dostępu do zasobów współdzielonych. Powyższy schemat przypomina okienka w dużej instytucji administracyjnej, na przykład na poczcie, gdzie przy każdym okienku urzędnik obsługuje klienta. Załóżmy, że od czasu do czasu urzędnicy ci muszą wykonać kserokopie dokumentów przyniesionych przez klientów, a do dyspozycji jest tylko jedna kserokopiarka. Dwóch pracowników nie może korzystać z kserokopiarki jednocześnie. Jeśli pracownik i zastał kserokopiarkę zajętą przez pracownika j, będzie musiał poczekać. Sytuację tę nazywamy dostępem do zasobu współdzielonego i w informatyce jest ona dość trudna do zarządzania. Weźmy następujący przykład:
- aplikacja wygeneruje n wątków, gdzie n jest przekazywane jako parametr
- zasób współdzielony to licznik, który musi być zwiększany przez każdy wygenerowany wątek
- Po zakończeniu działania aplikacji wyświetlana jest wartość licznika. Powinno się więc uzyskać n.
Program wygląda następująco:
' opcje
Option Explicit On
Option Strict On
' wykorzystanie wątków
Imports System
Imports System.Threading
Public Class thread3
' zmienne klasy
Private Shared cptrThreads As Integer = 0
Public Overloads Shared Sub Main(ByVal args() As [String])
' instrukcja obsługi
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' sprawdzanie liczby argumentów
If args.Length <> 1 Then
' błąd
Console.Error.WriteLine(syntaxe)
' zatrzymanie
Environment.Exit(1)
End If
' sprawdzanie jakości argumentu
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
' błąd
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
' koniec
Environment.Exit(2)
End Try
' tworzenie i generowanie wątków
Dim threads(nbThreads - 1) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' tworzenie
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' nazewnictwo
threads(i).Name = "tache_" & i
' uruchomienie
threads(i).Start()
Next i
' oczekiwanie na zakończenie wątków
For i = 0 To nbThreads - 1
threads(i).Join()
Next i ' affichage compteur
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' zwiększa licznik wątków
' odczyt licznika
Dim valeur As Integer = cptrThreads
' monitorowanie
Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' oczekiwanie
Thread.Sleep(1000)
' zwiększenie licznika
cptrThreads = valeur + 1
' monitorowanie
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
End Sub
End Class
Nie będziemy się zatrzymywać na części dotyczącej generowania wątków, którą już omówiliśmy. Skupmy się raczej na metodzie incrémente, wykorzystywanej przez każdy wątek do zwiększania wartości licznika statycznego cptrThreads.
- licznik jest odczytywany
- wątek zatrzymuje się na 1 s. W ten sposób traci dostęp do procesora
- licznik jest zwiększany
Krok 2 służy wyłącznie do wymuszenia utraty kontroli nad procesorem przez wątek. Kontrola ta zostanie przekazana innemu wątku. W praktyce nic nie gwarantuje, że wątek nie zostanie przerwany w okresie między odczytaniem wartości licznika a jego zwiększeniem o 1. Istnieje ryzyko utraty dostępu do procesora między odczytaniem wartości licznika a zapisaniem jego wartości zwiększonej o 1. Operacja zwiększania wartości licznika składa się bowiem z kilku instrukcji elementarnych na poziomie procesora, które mogą zostać przerwane. Krok 2, polegający na zawieszeniu na jedną sekundę, służy zatem wyłącznie usystematyzowaniu tego ryzyka. Uzyskane wyniki są następujące:
dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1
Analizując te wyniki, łatwo zrozumieć, co się dzieje:
- pierwszy wątek odczytuje licznik. Znajduje wartość 0.
- zatrzymuje się na 1 s, tracąc w ten sposób dostęp do procesora
- następnie drugi wątek przejmuje procesor i również odczytuje wartość licznika. Nadal wynosi ona 0, ponieważ poprzedni wątek jeszcze jej nie zwiększył. On również zatrzymuje się na 1 s.
- W ciągu 1 s wszystkie 5 wątków zdąży się uruchomić i odczytać wartość 0.
- Kiedy będą się budzić jeden po drugim, zwiększą odczytaną wartość 0 i zapiszą wartość 1 w liczniku, co potwierdza program główny (Main).
Skąd bierze się problem? Drugi wątek odczytał nieprawidłową wartość, ponieważ pierwszy został przerwany przed zakończeniem swojej pracy, polegającej na zaktualizowaniu licznika w oknie. To prowadzi nas do pojęcia zasobu krytycznego i sekcji krytycznej programu:
- zasób krytyczny to zasób, który w danym momencie może być posiadany tylko przez jeden wątek. W tym przypadku zasobem krytycznym jest licznik.
- Sekcja krytyczna programu to sekwencja instrukcji w strumieniu wykonania wątku, podczas której wątek uzyskuje dostęp do zasobu krytycznego. Należy zapewnić, aby w trakcie tej sekcji krytycznej tylko ten wątek miał dostęp do zasobu.
8.5. Wyłączny dostęp do zasobu współdzielonego
W naszym przykładzie sekcją krytyczną jest kod znajdujący się pomiędzy odczytem licznika a zapisem jego nowej wartości:
' odczyt licznika
Dim valeur As Integer = cptrThreads
' oczekiwanie
Thread.Sleep(1000)
' przyrost licznika
cptrThreads = valeur + 1
Aby wykonać ten kod, należy zapewnić, że wątek działa w izolacji. Może on zostać przerwany, ale podczas tej przerwy żaden inny wątek nie może wykonywać tego samego kodu. Platforma .NET oferuje kilka narzędzi zapewniających wyłączność dostępu do krytycznych sekcji kodu. Wykorzystamy klasę Mutex:

W tym przypadku wykorzystamy wyłącznie następujące konstruktory i metody:
tworzy obiekt synchronizacji M | |
Wątek T1, który wykonuje operację M.WaitOne(), żąda własności obiektu synchronizacji M. Jeśli muteks M nie jest posiadany przez żaden wątek (co ma miejsce na początku), zostaje on „przyznany” wątku T1, który o to poprosił. Jeśli nieco później wątek T2 wykona tę samą operację, zostanie zablokowany. W rzeczywistości muteks może należeć tylko do jednego wątku. Zostanie odblokowany, gdy wątek T1 zwolni muteks M, który posiada. W ten sposób wiele wątków może zostać zablokowanych w oczekiwaniu na muteks M. | |
Wątek T1, który wykonuje operację M.ReleaseMutex(), rezygnuje z posiadania muteksu M.Lorsque; wątek T1 straci dostęp do procesora, a system będzie mógł przydzielić go jednemu z wątków oczekujących na muteks M. Tylko jeden z nich otrzyma go w kolejności, pozostałe, oczekujące na M, pozostaną zablokowane |
Mutex M zarządza dostępem do współdzielonego zasobu R. Wątek żąda zasobu R za pomocą funkcji M.WaitOne() i zwraca go za pomocą funkcji M.ReleaseMutex(). Sekcja krytyczna kodu, która może być wykonywana tylko przez jeden wątek naraz, stanowi współdzielony zasób. Synchronizacja wykonywania sekcji krytycznej może przebiegać w następujący sposób:
gdzie M jest obiektem typu Mutex. Oczywiście należy zawsze pamiętać o zwolnieniu obiektu Mutex, który stał się zbędny, aby inny wątek mógł wejść do sekcji krytycznej; w przeciwnym razie wątki oczekujące na muteks, który nigdy nie został zwolniony, nigdy nie uzyskają dostępu do procesora. Ponadto należy unikać sytuacji wzajemnego blokowania (deadlock), w której dwa wątki czekają na siebie nawzajem. Rozważmy następujące działania, które następują po sobie w czasie:
- wątek T1 uzyskuje własność muteksu M1, aby uzyskać dostęp do zasobu współdzielonego R1
- wątek T2 uzyskuje własność muteksu M2, aby uzyskać dostęp do zasobu współdzielonego R2
- wątek T1 żąda muteksu M2. Zostaje zablokowany.
- Wątek T2 próbuje uzyskać dostęp do muteksu M1. Jest zablokowany.
W tym przypadku wątki T1 i T2 oczekują na siebie nawzajem. Sytuacja ta ma miejsce, gdy wątki potrzebują dwóch współdzielonych zasobów: zasobu R1 kontrolowanego przez muteks M1 oraz zasobu R2 kontrolowanego przez muteks M2. Jednym z możliwych rozwiązań jest jednoczesne żądanie obu zasobów za pomocą jednego muteksu M. Nie zawsze jest to jednak możliwe, jeśli na przykład wiąże się to z długotrwałym zajęciem kosztownego zasobu. Innym rozwiązaniem jest sytuacja, w której wątek posiadający zasób M1, który nie może uzyskać zasobu M2, zwalnia wówczas zasób M1, aby uniknąć wzajemnego blokowania. Jeśli zastosujemy to, co właśnie omówiliśmy, w poprzednim przykładzie, nasza aplikacja będzie wyglądać następująco:
' opcje
Option Explicit On
Option Strict On
' wykorzystanie wątków
Imports System
Imports System.Threading
Public Class thread4
' zmienne klasy
Private Shared cptrThreads As Integer = 0 ' compteur de threads
Private Shared autorisation As Mutex
Public Overloads Shared Sub Main(ByVal args() As [String])
' instrukcja obsługi
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' sprawdzanie liczby argumentów
If args.Length <> 1 Then
' błąd
Console.Error.WriteLine(syntaxe)
' zatrzymanie
Environment.Exit(1)
End If
' sprawdzanie jakości argumentu
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
End Try
' inicjalizacja uprawnień dostępu do sekcji krytycznej
autorisation = New Mutex
' tworzenie i generowanie wątków
Dim threads(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' tworzenie
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' nazywanie
threads(i).Name = "tache_" & i
' uruchomienie
threads(i).Start()
Next i
' oczekiwanie na zakończenie wątków
For i = 0 To nbThreads - 1
threads(i).Join()
Next i
' wyświetlanie licznika
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' zwiększa licznik wątków
' wnosimy o zezwolenie na wejście do sekcji krytycznej
autorisation.WaitOne()
' odczyt licznika
Dim valeur As Integer = cptrThreads
' monitorowanie
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' oczekiwanie
Thread.Sleep(1000)
' inkrementacja licznika
cptrThreads = valeur + 1
' monitorowanie
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
' przywracanie uprawnień dostępu
autorisation.ReleaseMutex()
End Sub
End Class
Uzyskane wyniki są zgodne z oczekiwaniami:
dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5
8.6. Synchronizacja oparta na zdarzeniach
Rozważmy następującą sytuację, nazywaną czasem sytuacją producentów i konsumentów.
- Mamy tablicę, do której niektóre procesy zapisują dane (producenci), a inne je odczytują (konsumenci).
- Producenci są sobie równi, ale działają na zasadzie wyłączności: w danym momencie tylko jeden producent może umieszczać swoje dane w tablicy.
- Konsumenci są sobie równi, ale wykluczają się wzajemnie: w danym momencie tylko jeden konsument może odczytywać dane zapisane w tablicy.
- Konsument może odczytać dane z tablicy tylko wtedy, gdy producent je tam umieścił, a producent może umieścić nowe dane w tablicy tylko wtedy, gdy te, które się w niej znajdują, zostały już wykorzystane.
W niniejszym omówieniu można wyróżnić dwa wspólne zasoby:
- tabela do zapisu
- tabela do odczytu
Dostęp do tych dwóch współdzielonych zasobów można kontrolować za pomocą muteksów, jak pokazano wcześniej, po jednym dla każdego zasobu. Gdy konsument uzyska dostęp do tabeli do odczytu, musi sprawdzić, czy rzeczywiście znajdują się w niej dane. W tym celu wykorzystamy zdarzenie, które go o tym powiadomi. Podobnie producent, który uzyskał dostęp do tabeli do zapisu, będzie musiał poczekać, aż konsument ją opróżni. Również w tym przypadku wykorzystamy zdarzenie.
Wykorzystywane zdarzenia będą częścią klasy AutoResetEvent:

Ten typ zdarzenia jest analogiczny do wartości logicznej, ale pozwala uniknąć oczekiwania aktywnego lub półaktywnego. Jeśli więc prawo do zapisu jest kontrolowane przez wartość logiczną peutEcrire, producent przed zapisaniem wykona kod w następującym stylu:
lub
W pierwszej metodzie wątek niepotrzebnie obciąża procesor. W drugiej co 100 ms sprawdza stan zmiennej logicznej peutEcrire. Klasa AutoResetEvent pozwala jeszcze bardziej usprawnić działanie: wątek poprosi o wybudzenie, gdy wystąpi oczekiwane zdarzenie:
AutoEvent peutEcrire=new AutoResetEvent(false) ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai
Operacja
inicjuje zmienną logiczną peutEcrire na wartość false. Operacja
wykonana przez wątek powoduje, że wątek ten przechodzi dalej, jeśli zmienna logiczna peutEcrire ma wartość prawdziwą, w przeciwnym razie zostaje zablokowany do momentu, aż zmienna ta przyjmie wartość prawdziwą. Inny wątek ustawi tę wartość na „prawda” za pomocą operacji peutEcrire.Set() lub na „fałsz” za pomocą operacji peutEcrire.Reset().
Program typu „producent-konsument” wygląda następująco:
' korzystanie z wątków odczytu i zapisu
' ilustruje jednoczesne korzystanie z zasobów współdzielonych i synchronizacji
' opcje
Option Explicit On
Option Strict On
' korzystanie z wątków
Imports System
Imports System.Threading
Public Class lececr
' zmienne klasowe
Private Shared data(5) As Integer ' ressource partagée entre threads lecteur et threads écrivain
Private Shared lecteur As Mutex ' variable de synchronisation pour lire le tableau
Private Shared écrivain As Mutex ' variable de synchronisation pour écrire dans le tableau
Private Shared objRandom As New Random(DateTime.Now.Second) ' un générateur de nombres aléatoires
Private Shared peutLire As AutoResetEvent ' signale qu'on peut lire le contenu de data
Private Shared peutEcrire As AutoResetEvent
Public Shared Sub Main(ByVal args() As [String])
' liczba wątków do wygenerowania
Const nbThreads As Integer = 3
' inicjalizacja flag
peutLire = New AutoResetEvent(False) ' on ne peut pas encore lire
peutEcrire = New AutoResetEvent(True) ' on peut déjà écrire
' inicjalizacja zmiennych synchronizacyjnych
lecteur = New Mutex ' synchronise les lecteurs
écrivain = New Mutex ' synchronise les écrivains
' tworzenie wątków odczytujących
Dim lecteurs(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' tworzenie
lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
lecteurs(i).Name = "lecteur_" & i
' uruchomienie
lecteurs(i).Start()
Next i
' tworzenie wątków zapisujących
Dim écrivains(nbThreads) As Thread
For i = 0 To nbThreads - 1
' tworzenie
écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
écrivains(i).Name = "écrivain_" & i
' uruchomienie
écrivains(i).Start()
Next i
'zakończenie operacji
Console.Out.WriteLine("fin de Main...")
End Sub
' odczyt zawartości tablicy
Public Shared Sub lire()
' sekcja krytyczna
lecteur.WaitOne() ' un seul lecteur peut passer
peutLire.WaitOne() ' on doit pouvoir lire
' odczyt tabeli
Dim i As Integer
For i = 0 To data.Length - 1
'oczekiwanie 1 s
Thread.Sleep(1000)
' wyświetlanie
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
Next i
' nie można już odczytać
peutLire.Reset()
' można zapisywać
peutEcrire.Set()
' koniec sekcji krytycznej
lecteur.ReleaseMutex()
End Sub
' zapis w tablicy
Public Shared Sub écrire()
' sekcja krytyczna
' może przejść tylko jeden pisarz
écrivain.WaitOne()
' należy poczekać na zezwolenie na zapis
peutEcrire.WaitOne()
' zapis do tablicy
Dim i As Integer
For i = 0 To data.Length - 1
'oczekiwanie 1 s
Thread.Sleep(1000)
' wyświetlanie
data(i) = objRandom.Next(0, 1000)
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
Next i
' nie można już zapisywać
peutEcrire.Reset()
' można odczytywać
peutLire.Set()
'koniec sekcji krytycznej
écrivain.ReleaseMutex()
End Sub
End Class
Wykonanie daje następujące wyniki:
dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C
Można zauważyć następujące kwestie:
- w danym momencie działa tylko jeden czytnik, mimo że traci on dostęp do procesora w sekcji krytycznej lire
- w danym momencie działa tylko jeden zapisujący, mimo że traci on dostęp do procesora w sekcji krytycznej écrire
- odczytujący odczytuje tylko wtedy, gdy w tablicy jest coś do odczytania
- zapisujący zapisuje tylko wtedy, gdy tablica została w całości odczytana

