Thread Synchronization Lock ManualResetEvent AutoResetEvent CountdownEvent and More






4.79/5 (9 votes)
Thread synchronization mechanisms and few other classes in the "System.Threading" namespace
Background
This note is not about multi-threading, it is about how to control the execution order of the code. With multiple threads running in a program, there is no guarantee of the execution order and if a thread will be interrupted by other threads. Thread synchronization is an inevitable challenge in virtually any multi-thread programs.
- If multiple threads try to access some shared resources or data, and if we need to make sure only one thread can access them at a time, we have the critical section problem;
- If some threads need to wait for other threads to inform them before they can continue, we need a mechanism to notify/signal the waiting threads when certain events happen.
The attached is a Visual Studio 2013 solution with a few unit tests to demonstrate the usage of the lock statement, and a few classes from the "System.Threading" namespace that we can use to synchronize the thread execution.
Hopefully, with these unit tests, you can get familiar with the syntax to use the lock
statement and the thread synchronization classes and their behaviors. To make my writing of the unit tests easier, I heavily used delegates. If you are not familiar with delegates, you can take a look at my earlier note.
The Lock Statement
As a warm-up session and also for the completeness of this note, let us take a look at the lock statement and how it protects the critical sections. As simple as it is, the critical section problem is probably the most encountered and mostly discussed problem in a multi-thread program.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace T_sync_u_test
{
[TestClass]
public class T_1_Lock_Test
{
[TestMethod]
public void A_Lock_Test()
{
object tlock = new object();
// Each thread will add 1 to the count in the test
int count = 0;
Action action_counter = () =>
{
lock (tlock)
{
// Without the lock block, there is a possibility
// that different thread can read the same value
// from "prev_count"
int prev_count = count;
int next_count = count + 1;
count = next_count;
}
};
// Start 3 threads and wait for them to finish
List<Task> tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_counter));
tasks.Add(Task.Factory.StartNew(action_counter));
tasks.Add(Task.Factory.StartNew(action_counter));
Task.WaitAll(tasks.ToArray());
// Three threads modified the count, so the count is guaranteed 3
Assert.AreEqual(3, count);
}
}
}
- In the test method
A_Lock_Test()
, the "Action" delegateaction_counter
increases thecount
variable by1
; - Three concurrent threads are started to execute the
action_counter
delegate; - When all the threads complete, we should expect the
count = 3
.
Incrementing an integer may not always be an atomic operation. It involves reading the old value, adding 1 and updating the integer. If more than one thread reads the same old value, the end result will not be what we expected when all the threads finish.
- When a thread starts a
lock(tlock){...}
section, it will try to obtain an exclusive lock on the lock objecttlock
. If no other thread currently holding the lock object, it will get the lock and continue to execute the code in the lock block. Otherwise, it will be blocked until the other thread releases the lock object when finishing the code in its lock block; - When multiple threads trying to hold the lock object, there is no guarantee which thread can obtain the lock. In a program, we should not make any assumption that any thread will get the lock before others while multiple threads are competing for it.
The ManualResetEvent
In a multi-thread program, the ManualResetEvent
class can be used by a thread to inform other waiting threads to proceed when an event happens. A ManualResetEvent
object behaves like a door
that has two states.
- The
ManualResetEvent.Reset()
method closes the door; - The
ManualResetEvent.Set()
method opens the door; - If the door is open, the
bool ManualResetEvent.WaitOne(int millisecondsTimeout)
method will returntrue
immediately. If the door is closed, it will returnfalse
after themillisecondsTimeout
; - If the
millisecondsTimeout
is not specified, the overloaded methodvoid ManualResetEvent.WaitOne()
will be blocked indefinitely until the door is open.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace T_sync_u_test
{
[TestClass]
public class T_2_ManualResetEvent_Test
{
[TestMethod]
public void B_ManualResetEvent_Test()
{
// Initiate a ManualResetEvent with door closed (not set)
ManualResetEvent mre = new ManualResetEvent(false);
object tlock = new object();
int count = 0;
Action action_pass_counter = () =>
{
// WaitOne() returns true if ManualResetEvent is set,
// returns false if ManualResetEvent is not set, but due to
// a timeout (2 * 1000 milliseconds)
if (mre.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
// ********** Test No.1 **********
// Start 3 threads and wait them to finish
List<Task> tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// Because the ManualResetEvent is not set (closed),
// count++ is not executed, count = 0
Assert.AreEqual(0, count);
// ********** Test No.2 **********
// Open the ManualResetEvent door
mre.Set();
// Start 3 threads and wait them to finish
tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// Since the ManualResetEvent is set (open),
// count++ is executed, count = 3
Assert.AreEqual(3, count);
// ********** Test No.3 **********
// To close the ManualResetEvent, we need to call the rest()
// method manually
mre.Reset();
// Start 3 threads and wait them to finish
tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// Since the ManualResetEvent is re-set (closed),
// count++ is not executed, count remains 3
Assert.AreEqual(3, count);
}
}
}
- In the test method
B_ManualResetEvent_Test()
, an instance of theManualResetEvent
class is initiated as closed; - The Action delegate
action_pass_counter
increases thecount
variable by1
if theManualResetEvent
is open; - In the test No.1, the door is closed so the
count
variable remains0
when all the threads complete; - In the test No.2, the door is opened by the
ManualResetEvent.Set()
method. When all the threads complete, thecount = 3
; - In the test No.3, the
ManualResetEvent.Reset()
closes the door. When all the threads complete, the count remains3
.
It should be noted that the ManualResetEvent.Reset()
and the ManualResetEvent.Set()
methods can be called in any thread to close or open the door. If the door is open, it remains open and allows any number of threads to pass it. If the door is closed, it remains closed and blocks any thread from passing it until it reaches the timeout if specified.
The AutoResetEvent
The AutoResetEvent
class has a similar behavior as the ManualResetEvent
class, but it is not a door
. It is a "toll booth" which allows one and only one thread to pass it when it is open and then closes itself immediately.
- The
AutoResetEvent.Reset()
method closes the toll booth; - The
AutoResetEvent.Set()
method opens the toll booth; - When the toll booth is closed, it blocks any thread to pass it. When a timeout is specified on the overloaded method
bool AutoResetEvent.WaitOne(int millisecondsTimeout)
, it is blocked until the timeout is reached; - If the toll booth is open, it allow one and only one thread to pass it and closes itself immediately in an atomic fashion.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace T_sync_u_test
{
[TestClass]
public class T_3_AutoResetEvent_Test
{
[TestMethod]
public void C_AutoResetEvent_Test()
{
AutoResetEvent are = new AutoResetEvent(false);
object tlock = new object();
int count = 0;
Action action_pass_counter = () =>
{
// WaitOne() returns true if AutoResetEvent is set,
// returns false if AutoResetEvent is not set, but due to
// a timeout (2 * 1000 milliseconds)
if (are.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
// ********** Test No.1 **********
// Start 3 threads and wait them to finish
List<Task> tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// Because the AutoResetEvent is not set (closed),
// count++ is not executed, count = 0
Assert.AreEqual(0, count);
// ********** Test No.2 **********
// Open the AutoResetEvent door
are.Set();
// Start 3 threads and wait them to finish
tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// Since the AutoResetEvent is set (open), count++ can be executed.
// but the AutoResetEvent only allows a single thread to pass the door.
// Once a thread passes the door, AutoResetEvent will be closed atomically.
// count = 1
// AutoResetEvent bahaves like a toll-booth
Assert.AreEqual(1, count);
// ********** Test No.3 **********
// Once the AutoResetEvent toll-booth is closed, it remains closed
// unless a Set() is issued to open it
tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
tasks.Add(Task.Factory.StartNew(action_pass_counter));
Task.WaitAll(tasks.ToArray());
// The count remains 1 because the AutoResetEvent is closed (Reset)
Assert.AreEqual(1, count);
}
}
}
- In the test method
C_AutoResetEvent_Test()
, an instance of theAutoResetEvent
class is initiated as closed; - The Action delegate
action_pass_counter
increases thecount
variable by1
if theAutoResetEvent
is open; - In the test No.1, the toll booth is closed so the
count
variable remains0
when all the threads complete; - In the test No.2, the toll booth is opened by the
AutoResetEvent.Set()
method. When all the threads complete, thecount = 1
because the toll booth only allows 1 thread to pass it and closes itself immediately in an atomic fashion; - In the test No.3, the count remains
1
when all the threads complete. If a toll booth is closed, it remains closed until anAutoResetEvent.Set()
to open it.
The CountdownEvent
The CountdownEvent
class is similar to the ManualResetEvent
class. it behaves like a door
. We can use the CountdownEvent.Signal()
method to open the door.
- When initiating an
CountdownEvent(int initialCount)
instance, it is mandatory to specify an integerinitialCount
value. TheinitialCount
specifies the number of theCountdownEvent.Signal()
calls needed to open the door; - When the door is closed, it blocks any thread from passing it;
- When the door is open, it allows any thread to pass it;
- When the door is open, it remains open until the
CountdownEvent.Reset()
method is called to close it; - The
CountdownEvent.Signal()
method can be called in any thread.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
using System.Threading.Tasks;
namespace T_sync_u_test
{
[TestClass]
public class T_4_CountdownEvent_Test
{
[TestMethod]
public void D_CountdownEvent_Test()
{
// Initiate a CountdownEvent that needs 3 signals to set (open)
CountdownEvent cde = new CountdownEvent(3);
Func<bool> func_is_blocked = () =>
{
bool blocked = true;
// The Wait method returns true if CountdownEvent is set
// by getting enough signals (3). It returns false, if it
// reaches the timeout (2 * 1000 milliseconds)
if (cde.Wait(2 * 1000)) { blocked = false; }
return blocked;
};
// ********** Test No.1 **********
// Since there is no sigal, the CountdownEvent.Wait() method
// will timeout (blocked == true)
Assert.IsTrue(func_is_blocked());
// Start 3 threads. Each give the CountdownEvent a signal
Action action_issue_signal = () => { cde.Signal(); };
Task.Run(action_issue_signal);
Task.Run(action_issue_signal);
Task.Run(action_issue_signal);
// ********** Test No.2 **********
// The CountdownEvent has 3 signals, the CountdownEvent.Wait()
// returns immediately with true.
Assert.IsFalse(func_is_blocked());
// ********** Test No.3 **********
// Once the door is open, it remains open until reset by the program
Assert.IsFalse(func_is_blocked());
// ********** Test No.4 **********
// Calling the Signal() method more than the initial value on the
// CountdownEvent causes an exception
bool exceptionThrown = false;
try { cde.Signal(); }
catch (Exception) { exceptionThrown = true; }
Assert.IsTrue(exceptionThrown);
}
}
}
- In the test method
D_CountdownEvent_Test()
, an instance of theCountdownEvent
class is initiated withinitialCount = 3
; - The "Func<bool>" delegate
func_is_blocked
returnstrue
if the door is closed,false
if the door is open; - The Action delegate
action_issue_signal
issues aCountdownEvent.Signal()
to theCountdownEvent
instance; - In the test No.1, the
func_is_blocked
returnstrue
because the door is closed; - In the test No.2, the
func_is_blocked
returnsfalse
because 3 threads are running to signal the door to open; - In the test No.3, the
func_is_blocked
returnsfalse
. It shows us that if the door is open it remains open, until theCountdownEvent.Reset()
method is called to close it; - In the test No.4, we get an exception. It shows us that if we try to signal the
CountdownEvent
instance more than theinitialCount
times, we will receive an exception.
The EventWaitHandle
An EventWaitHandle
class can be used as a ManualResetEvent
. It can also be used as an AutoResetEvent
.
- When initiated with
EventResetMode.ManualReset
, it behaves exactly like aManualResetEvent
; - When initiated with
EventResetMode.AutoReset
, it behaves exactly like anAutoResetEvent
.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace T_sync_u_test
{
[TestClass]
public class T_5_EventWaitHandle_Test
{
[TestMethod]
public void E_EventWaitHandle_Test()
{
// EventWaitHandle
EventWaitHandle ewh_manual
= new EventWaitHandle(false, EventResetMode.ManualReset);
Func<bool> func_manual_is_blocked = () =>
{
bool blocked = true;
if (ewh_manual.WaitOne(2 * 1000)) { blocked = false; }
return blocked;
};
// ********** Test No.1 **********
// When initiate with EventResetMode.ManualReset, EventWaitHandle
// bahaves like ManualResetEvent. Once the door is open, it remains
// open, until manually reset
ewh_manual.Set();
Assert.IsFalse(func_manual_is_blocked());
Assert.IsFalse(func_manual_is_blocked());
// Reset the EventWaitHandle, it blocks the func_manual()
ewh_manual.Reset();
Assert.IsTrue(func_manual_is_blocked());
// EventWaitHandle
EventWaitHandle ewh_auto
= new EventWaitHandle(false, EventResetMode.AutoReset);
int count = 0;
object tlock = new object();
Action action_auto_pass_counter = () =>
{
// If not blocked, add count by 1
if (ewh_auto.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
// ********** Test No.2 **********
// When initiate with EventResetMode.AutoReset, EventWaitHandle
// behaves like AutoResetEvent. When the door is open, it only
// allows a single thread to pass and then closed (reset) atomically
ewh_auto.Set();
List<Task> tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
Task.WaitAll(tasks.ToArray());
// Since only one thread can pass WaitOne(), the
// count = 1
Assert.AreEqual(1, count);
// ********** Test No.3 **********
// Since the door is closed, no thread can go through,
// the count remains 1
tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
tasks.Add(Task.Factory.StartNew(action_auto_pass_counter));
Task.WaitAll(tasks.ToArray());
Assert.AreEqual(1, count);
}
}
}
- The
Func<bool>
func_manual_is_blocked
returnstrue
if the thread is blocked; - The test No.1 shows that if we initiate the
EventWaitHandle
withEventResetMode.ManualReset
, we need to manually open and close the door; - The Action
action_auto_pass_counter
increase the count variable by1
if the door is open; - The test No.2 shows that if we initiate the
EventWaitHandle
withEventResetMode.AutoReset
, it allows one and only one thread to pass it if it is open and closes itself immediately in an atomic fashion; - The Test No.3 show that if the
EventWaitHandle
is closed, it remains closed until it is open by theEventWaitHandle.Set()
method.
The ManualResetEventSlim
We have seen the ManualResetEvent
class. But there is another class called ManualResetEventSlim
. According to Microsoft:
You can use this class for better performance than ManualResetEvent when wait times are expected to be very short, and when the event does not cross a process boundary. ManualResetEventSlim uses busy spinning for a short time while it waits for the event to become signaled. When wait times are short, spinning can be much less expensive than waiting by using wait handles. However, if the event does not become signaled within a certain period of time, ManualResetEventSlim resorts to a regular event handle wait.
If the waiting threads expect the door can be opened in a short time, and if the door is within a process, we can use the ManualResetEventSlim
for better performance. But if the waiting threads need to wait for a long time before the door is open. The ManualResetEvent
may not be an appropriate choice.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
namespace T_sync_u_test
{
[TestClass]
public class T_6_ManualResetEventSlim_Test
{
[TestMethod]
public void F_ManualResetEventSlim_Test()
{
ManualResetEventSlim mres = new ManualResetEventSlim(false);
Func<bool> func_mres_is_blocked = () =>
{
bool blocked = true;
if (mres.Wait(2 * 1000)) { blocked = false; }
return blocked;
};
// ********** Test No.1 **********
// Since the ManualResetEventSlim is initiated as closed (not set),
// it blocks the thread.
Assert.IsTrue(func_mres_is_blocked());
// ********** Test No.2 **********
// Set the ManualResetEventSlim opens the door, and it remains open
// until it is closed (reset)
mres.Set();
Assert.IsFalse(func_mres_is_blocked());
Assert.IsFalse(func_mres_is_blocked());
// ********** Test No.3 **********
// Reset the ManualResetEventSlim closes the door, it remains closed
// if not opened by Set()
mres.Reset();
Assert.IsTrue(func_mres_is_blocked());
Assert.IsTrue(func_mres_is_blocked());
}
}
}
- The
Func<bool>
func_mres_is_blocked
returnstrue
if the door is closed,false
if the door is open; - The test No.1 shows that the thread is blocked when the door is closed;
- The test No.2 shows that the door remains open once it is open;
- The test No.3 shows that we can use the
ManualResetEventSlim.Reset()
method to close the door. The door remains closed once it is closed until we open it byManualResetEventSlim.Set()
.
Run the Unit Tests
If you load the solution in Visual Studio, you can run the unit tests. The following shows the test results in Visual Studio 2013.
Points of Interest
- This is a quick note and a set of unit tests on the thread synchronization mechanisms using the
lock
statement and a few classes in theSystem.Threading
namespace; - A common mistake to synchronize the execution of multiple threads is to use the "Thread.Sleep()" method, because we have no way to estimate how long the threads need to sleep before they can proceed. We should choose among the thread synchronization classes
ManualResetEvent
,AutoResetEvent
,CountdownEvent
,EventWaitHandle
, andManualResetEventSlim
to control the execution order among the multiple threads; - I hope you like my posts and I hope this note can help you in one way or the other.
History
- 30th July, 2016: First revision