Synchronous & Asynchronous and MultiThread Programming -5 — Communication between threads: Signalling

Alperen Öz
8 min readJun 14, 2024

--

Communication between threads: Signalling

Photo by Campaign Creators on Unsplash

What is Signalling? What is it Used for?

  • Signalling is a mechanism used in multi-threading programming to facilitate communication and synchronization between threads.
  • It allows one thread to notify another thread that a specific event has occurred, enabling the continuation of a process based on this notification.
  • These events can include the completion of a task by a thread, the fulfillment of a condition, or the availability of a resource.
  • Signalling provides a more optimized approach to handling race conditions in multi-threading programming, controls access to critical sections, and ensures safe sharing between threads.
  • It allows threads to inform each other about changes in these conditions during their operation processes.

In summary, signalling is an essential technique used for communication and coordination between threads.

Tools Used in Signalling Approaches

  • AutoResetEvent: Allows a thread to wait for a specific event to occur, and when that event happens, the thread automatically resumes its operation.
  • ManualResetEventSlim: Enables multiple threads to wait for a specific event to occur. It provides a manually resettable event signal.
  • CountdownEvent: Used to wait for a specified number of threads to complete their operations.

What is AutoResetEvent?

  • AutoResetEvent is a signalling and synchronization tool that allows a thread to wait for a specific event to occur and then continue its operation once that event happens.
  • When the event occurs, the AutoResetEvent signal is sent to only one waiting thread, even if multiple threads are waiting. After sending the signal, it automatically resets, allowing only a single thread to proceed. Here are some critical features:

1. Single Thread Continues:

AutoResetEvent ensures that only one waiting thread continues its operation when the signal is sent.

Turnstile Metaphor:

  • AutoResetEvent operates like a turnstile, allowing only one thread to proceed at a time from the waiting threads.
  • Just as a turnstile allows one person to pass and then resets to block others until the next signal, AutoResetEvent signals one thread to continue while others remain waiting, maintaining the order of operations.

2. Automatic Reset:

After the event occurs and the signal is sent, AutoResetEvent automatically resets. This ensures that other threads wait for the signal again before proceeding.

3. Queue Formation:

If multiple threads call the WaitOnemethod, AutoResetEvent forms a queue and processes threads in order. When a signal arrives, the next thread in the queue continues its operation.

int i = 0;
AutoResetEvent autoResetEvent = new(false); // Initial state parameter => false
Thread thread1 = new(() =>
{
while (i <= 10)
{
i++;
Console.WriteLine("Thread1:" + i);
}
autoResetEvent.Set(); // Signal that the task is completed...
});
Thread thread2 = new(() =>
{
autoResetEvent.WaitOne(); // A thread waiting for a signal to start working...
while (i < 20)
{
i++;
Console.WriteLine("Thread2:" + i);
}
autoResetEvent.Set(); // Signal that the task is completed...
});
Thread thread3 = new(() =>
{
autoResetEvent.WaitOne(); // A thread waiting for a signal to start working...
while (i < 30)
{
i++;
Console.WriteLine("Thread3:" + i);
}
autoResetEvent.Set(); // Signal that the task is completed...
});

thread1.Start();
thread2.Start();
thread3.Start();

// output
// Thread1:1
// Thread1:2
// Thread1:3
// Thread1:4
// Thread1:5
// Thread1:6
// Thread1:7
// Thread1:8
// Thread1:9
// Thread1:10
// Thread1:11
// Thread2:12
// Thread2:13
// Thread2:14
// Thread2:15
// Thread2:16
// Thread2:17
// Thread2:18
// Thread2:19
// Thread2:20
// Thread3:21
// Thread3:22
// Thread3:23
// Thread3:24
// Thread3:25
// Thread3:26
// Thread3:27
// Thread3:28
// Thread3:29
// Thread3:30
  • The AutoResetEvent object must be configured to signal from one thread to another. There are two ways to achieve this: The behavior differs depending on whether we pass a value of true or false to the initialState parameter.

initialState = false

  • When the initialState parameter is set to false, the AutoResetEvent object is initially in a state where it is waiting for a signal.
  • By setting the value to false, we indicate that this object is ready to send a signal using the signaling tool.
  • We can directly send a signal by calling autoResetEvent.Set() without any additional operations on the AutoResetEvent object.

Behavior:

  • Thread 1 starts, and after printing the “Thread1” message, it signals by calling the autoResetEvent.Set() method.
  • Thread 2 starts and waits for a signal from Thread 1 using the autoResetEvent.WaitOne() method. Once it receives the signal, it prints the "Thread2" message and signals the next thread by calling the autoResetEvent.Set() method.
  • Thread 3 starts and waits for a signal from Thread 2 using the autoResetEvent.WaitOne() method. Once it receives the signal, it prints the "Thread3" message and signals the next thread by calling the autoResetEvent.Set() method.

Given the nature of AutoResetEvent, after Thread 1 starts, the signal from Thread 1 will reach only one thread, allowing only that single thread to proceed with its operation. As seen in the output, the thread that starts processing after receiving the signal from Thread 1 could be either Thread 2 or Thread 3 in a given run. This means the output sequence could also be in the order of Thread1-Thread3-Thread2.

AutoResetEvent autoResetEvent = new(true);   
Thread thread1 = new(() =>
{
autoResetEvent.Reset(); //set for wait signal with reset method...
Console.WriteLine("Thread1");
autoResetEvent.Set();
});
Thread thread2 = new(() =>
{
autoResetEvent.Reset();
autoResetEvent.WaitOne();
Console.WriteLine("Thread2");
autoResetEvent.Set();
});
Thread thread3 = new(() =>
{
autoResetEvent.Reset();
autoResetEvent.WaitOne();
Console.WriteLine("Thread3");
autoResetEvent.Set();
});

thread1.Start();
thread2.Start();
thread3.Start();
  • When the AutoResetEvent object is initialized with true, it starts in a signaled state. However, when all threads are started, their initial operations should call the autoResetEvent.Reset() method to reset the signal, putting them into a wait state.

If the initialState parameter is set to false, the AutoResetEvent object starts in a state where it is waiting for a signal and can directly send a signal using the autoResetEvent.Set() method.

If the initialState parameter is set to true, the AutoResetEvent object starts in a signaled state. This means you must manually call autoResetEvent.Reset() to put it into a wait state, ready to signal.

The case where the initialState parameter is set to true is quite rare. Typically, it is more common to initialize the AutoResetEvent object with false.

What is ManualResetEventSlim

  • ManualResetEventSlim is a class used for signaling and synchronization between threads. Unlike AutoResetEvent, which automatically resets after signaling, ManualResetEventSlim requires manual resetting after signaling until all waiting threads are allowed to proceed. This class is touted as a more efficient and lower resource-consuming version of the ManualResetEvent.
  • In other words, ManualResetEventSlim will initiate the operation for one or more waiting threads when signaled. After signaling, all waiting threads wake up and proceed with the operation due to the signal. If control over the awakened threads is desired, the Reset() function is used to reset the event.

Features and Differences:

  • Manual Resetting: ManualResetEventSlim allows all waiting threads to continue working until manually reset after signaling.
  • Performance: Compared to ManualResetEvent, ManualResetEventSlim offers lower resource consumption and higher performance.
  • Releasing Waiting Threads: When the Set() method is called, a signal is given, releasing all waiting threads. The signal state persists until the Reset() method is called.
int i = 0;
ManualResetEventSlim manualResetEventSlim = new(false);
Thread thread1 = new(() =>
{
while (i++ < 10)
{
Console.WriteLine("Thread1:" +i);
}
manualResetEventSlim.Set(); //sends signal to other waiting threads...
manualResetEventSlim.Reset(); //signal cuts off...
});
Thread thread2 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 20)
{
Console.WriteLine("Thread2:" + i);
}
});
Thread thread3 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 30)
{
Console.WriteLine("Thread3:" + i);
}
});

thread1.Start();
thread2.Start();
thread3.Start();

//output
//Thread1:1
//Thread1:2
//Thread1:3
//Thread1:4
//Thread1:5
//Thread1:6
//Thread1:7
//Thread1:8
//Thread1:9
//Thread1:10
  • As seen in the example, calling manualResetEventSlim.Set(); followed by manualResetEventSlim.Wait(); activates all other waiting threads. Subsequently, by executing manualResetEventSlim.Reset();, this signal is terminated.

The reason why only the result of the loop in thread1 is observed in the output of the example is that after thread1 completes its loop, manualResetEventSlim.Set(); activates the other waiting threads, but manualResetEventSlim.Reset(); prevents them from executing their tasks quickly enough.

int i = 0;
ManualResetEventSlim manualResetEventSlim = new(false);
Thread thread1 = new(() =>
{
while (i++ < 10)
{
Console.WriteLine("Thread1:" +i);
}
manualResetEventSlim.Set();
//Other threads are given the opportunity to run before the reset...
Thread.Sleep(100);
manualResetEventSlim.Reset();
});
Thread thread2 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 20)
{
Console.WriteLine("Thread2:" + i);
}
});
Thread thread3 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 30)
{
Console.WriteLine("Thread3:" + i);
}
});

thread1.Start();
thread2.Start();
thread3.Start();

//output
//Thread1:1
//Thread1:2
//Thread1:3
//Thread1:4
//Thread1:5
//Thread1:6
//Thread1:7
//Thread1:8
//Thread1:9
//Thread1:10
//Thread2:12
//Thread2:14
//Thread2:15
//Thread2:16
//Thread2:17
//Thread2:18
//Thread2:19
//Thread2:20
//Thread3:13
//Thread3:22
//Thread3:23
//Thread3:24
//Thread3:25
//Thread3:26
//Thread3:27
//Thread3:28
//Thread3:29
//Thread3:30

As explained in the comment above the example, by adding Thread.Sleep() between manualResetEventSlim.Set(); and manualResetEventSlim.Reset();, the code delays reaching manualResetEventSlim.Reset();. This allows other waiting threads (thread1 and thread2) to execute.

The working principle of ManualResetEventSlim can be likened to a door. When an event occurs, signaled by the Set method, it’s as if the door opens, allowing all waiting threads to enter (or execute). Subsequently, calling Reset method closes this door, preventing further threads from entering until the event is signaled again.

EventWaitHandle

  • EventWaitHandle class allows us to manage the behaviors of ManualResetEventSlim and AutoResetEvent classes through a single instance.
  • When creating an instance of EventWaitHandle class, the second parameter specifies which behavior (ManualResetEventSlim or AutoResetEvent) we want the signaling object to exhibit.
EventWaitHandle eventWaitHandle = new(false, EventResetMode.AutoReset); //AutoResetEvent 
EventWaitHandle eventWaitHandle = new(false, EventResetMode.ManualReset); //ManualResetEventSlim
Thread thread1 = new(() =>
{
Console.WriteLine("Thread1");
eventWaitHandle.Set();
});
Thread thread2 = new(() =>
{
eventWaitHandle.WaitOne();
Console.WriteLine("Thread2");
});
Thread thread3 = new(() =>
{
eventWaitHandle.WaitOne();
Console.WriteLine("Thread3");
});

thread1.Start();
thread2.Start();
thread3.Start();

//autoReset output
//Thread1
//Thread2

//ManuelReset output
//Thread1
//Thread2
//Thread3
  • As observed in the output, when the second parameter of the eventWaitHandle object is EventResetMode.AutoReset, it exhibits the behavior of AutoResetEvent. Thread1 activates, followed by only one thread becoming active, and Thread2 executes.
  • When the second parameter is EventResetMode.ManualReset, it demonstrates the behavior of ManualResetEventSlim. After Thread1 signals, all waiting threads begin their execution.

CountDownEvent

  • CountdownEvent is a tool that exhibits behavior based on signaling after a specified number of threads complete their operations.
  • It is useful in scenarios where a large file needs to be processed in segments, and completion of all segments is awaited, or when multiple network calls need to be completed before proceeding.
//waiting for the task completion signal from 3 threads...
CountdownEvent countdownEvent = new(3);

Thread thread1 = new(() =>
{
Console.WriteLine("Thread1");
Thread.Sleep(1000);
countdownEvent.Signal(); //job done..=> 2.signal
});
Thread thread2 = new(() =>
{
Console.WriteLine("Thread2");
Thread.Sleep(5500);
countdownEvent.Signal(); ////job done..=> 3.signal
});
Thread thread3 = new(() =>
{
Console.WriteLine("Thread3");
Thread.Sleep(800);
countdownEvent.Signal(); //job done..=> 1.signal
});
Thread thread4 = new(() =>
{
//It is waiting for a signal to work... It is active after the 3rd signal
countdownEvent.Wait();
Console.WriteLine("Thread4");
});

thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();

//output
//Thread1
//Thread2
//Thread3
//Thread4
  • As seen in the example, the CountdownEvent object is initialized with an initial count of 3 threads.
  • After signals are received from each of thread1, thread2, and thread3, the thread4, which is waiting with countdownEvent.Wait();, will become active after receiving signals from all 3 different threads.
  • To illustrate this waiting process more clearly, Thread.Sleep delays have been added before countdownEvent.Signal(); calls in the threads.
  • Even if thread1 and thread2 complete and signal after their respective Thread.Sleep delays, thread4 still needs to wait for the completion signal from thread3. Therefore, thread4 will become active 5500 ms later.

The main thread can also be delayed with countdownEvent.Wait();.

Source: Gençay Yıldız — Asenkron & Multithread Programlama

--

--