Sygnalizacja (Signaling) #
Mechanizmy sygnalizacji służą do komunikacji między wątkami. Pozwalają jednemu wątkowi wstrzymać działanie do momentu otrzymania powiadomienia (sygnału) od innego wątku.
EventWaitHandle i CountdownEvent #
Klasy pochodne od EventWaitHandle (ManualResetEvent, AutoResetEvent) działają jak bramki, które mogą być otwarte (sygnalizowane) lub zamknięte (niesygnalizowane). CountdownEvent działa na innej zasadzie (odwrócony licznik) i nie dziedziczy po EventWaitHandle.
Set(): Otwiera bramkę (ustawia stan na zasygnalizowany).Reset(): Zamyka bramkę (ustawia stan na niezasygnalizowany).WaitOne(): Czeka przy bramce. Jeśli jest otwarta, przechodzi natychmiast. Jeśli zamknięta, blokuje wątek do momentu otwarcia.
CountdownEventużywa metodSignal()(dekrementacja) iWait()(oczekiwanie na zero).
Rodzaje zdarzeń #
ManualResetEvent(Slim): Po otwarciu (Set) pozostaje otwarta dla dowolnej liczby wątków, dopóki nie zostanie ręcznie zamknięta (Reset). Działa jak klasyczna brama.AutoResetEvent: Po otwarciu przepuszcza tylko jeden czekający wątek i natychmiast automatycznie się zamyka. Działa jak bramka w metrze.CountdownEvent: Zostaje zasygnalizowany dopiero, gdy jego wewnętrzny licznik spadnie do zera. Każde wywołanieSignal()zmniejsza ten licznik. Jest przydatny w sytuacjach, gdy jeden wątek musi czekać na zakończenie pracy przez określoną liczbę innych wątków.
Przykład: Kolejka Producent-Konsument #
Poniższy przykład implementuje kolejkę o ograniczonej pojemności, używając ManualResetEvent do sygnalizowania, czy kolejka jest pełna, czy pusta.
Zauważ pętlę
while(true)w metodachEnqueueiDequeue. Jest ona konieczna, ponieważ pomiędzy wywołaniemWaitOne()(sygnał: jest miejsce) a wejściem wlockinny wątek mógł zająć to miejsce. Wymaga to ponownego sprawdzenia warunku.
public class Queue<T> : IDisposable
{
private readonly T?[] _array;
private int _head;
private int _tail;
private int _count;
private readonly ManualResetEvent _notEmpty;
private readonly ManualResetEvent _notFull;
private readonly Lock _lock;
public Queue(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity));
}
_array = new T[capacity];
_head = 0;
_tail = 0;
_count = 0;
_notEmpty = new ManualResetEvent(false);
_notFull = new ManualResetEvent(true);
_lock = new Lock();
}
public void Enqueue(T item)
{
while (true)
{
_notFull.WaitOne();
lock (_lock)
{
if (_count < _array.Length)
{
_array[_tail] = item;
_tail = (_tail + 1) % _array.Length;
_count++;
if (_count == _array.Length)
{
_notFull.Reset();
}
_notEmpty.Set();
return;
}
}
}
}
public T Dequeue()
{
while (true)
{
_notEmpty.WaitOne();
lock (_lock)
{
if (_count > 0)
{
T item = _array[_head]!;
_array[_head] = default;
_head = (_head + 1) % _array.Length;
_count--;
if (_count == 0)
{
_notEmpty.Reset();
}
_notFull.Set();
return item;
}
}
}
}
public void Dispose()
{
_notEmpty.Dispose();
_notFull.Dispose();
}
}Kod źródłowy:
Barrier #
Klasa Barrier służy do synchronizacji grupy wątków, które muszą pracować w fazach. Wątki dochodzą do bariery (SignalAndWait) i czekają, aż wszystkie pozostałe z grupy również tam dotrą. Dopiero gdy komplet wątków się zamelduje, wszystkie są zwalniane do kolejnej fazy.
Przykład: Gra w kości #
W tym przykładzie 3 wątki rzucają kośćmi. Bariera zapewnia, że wszystkie wątki wykonają rzut w tej samej rundzie, zanim którakolwiek przejdzie do następnej, a wyniki zostaną wypisane w jednej linii.
class Program
{
static void Main(string[] args)
{
var barrier = new Barrier(3, _ => Console.WriteLine());
new Thread(RollDice).Start();
new Thread(RollDice).Start();
new Thread(RollDice).Start();
void RollDice()
{
for (int i = 0; i < 5; i++)
{
Console.Write($"{D6()} ");
barrier.SignalAndWait();
}
}
}
static int D6() => 1 + Random.Shared.Next(6);
}Kod źródłowy:
Monitor.Wait / Monitor.Pulse #
Jest to mechanizm zmiennych warunkowych (powiązany z lock).
Monitor.Wait(obj): Zwalnia blokadę na obiekcieobji usypia wątek do momentu otrzymania powiadomienia.Monitor.Pulse(obj): Budzi jeden wątek czekający na obiekcieobj.Monitor.PulseAll(obj): Budzi wszystkie czekające wątki.
Jest to mechanizm bardziej niskopoziomowy niż EventWaitHandle i wymaga przebywania wewnątrz sekcji lock. Zazwyczaj użycie EventWaitHandle jest wystarczające i prostsze.