This is a quick note on the thread synchronization mechanisms using lock statements, ManualResetEvent, AutoResetEvent, CountdownEvent, and a 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();
int count = 0;
Action action_counter = () =>
{
lock (tlock)
{
int prev_count = count;
int next_count = count + 1;
count = next_count;
}
};
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());
Assert.AreEqual(3, count);
}
}
}
- In the test method
A_Lock_Test()
, the "Action" delegate action_counter
increases the count
variable by 1
; - 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 object tlock
. 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 return true
immediately. If the door is closed, it will return false
after the millisecondsTimeout
; - If the
millisecondsTimeout
is not specified, the overloaded method void 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()
{
ManualResetEvent mre = new ManualResetEvent(false);
object tlock = new object();
int count = 0;
Action action_pass_counter = () =>
{
if (mre.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
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());
Assert.AreEqual(0, count);
mre.Set();
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());
Assert.AreEqual(3, count);
mre.Reset();
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());
Assert.AreEqual(3, count);
}
}
}
- In the test method
B_ManualResetEvent_Test()
, an instance of the ManualResetEvent
class is initiated as closed; - The Action delegate
action_pass_counter
increases the count
variable by 1
if the ManualResetEvent
is open; - In the test No.1, the door is closed so the
count
variable remains 0
when all the threads complete; - In the test No.2, the door is opened by the
ManualResetEvent.Set()
method. When all the threads complete, the count = 3
; - In the test No.3, the
ManualResetEvent.Reset()
closes the door. When all the threads complete, the count remains 3
.
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 = () =>
{
if (are.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
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());
Assert.AreEqual(0, count);
are.Set();
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());
Assert.AreEqual(1, count);
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());
Assert.AreEqual(1, count);
}
}
}
- In the test method
C_AutoResetEvent_Test()
, an instance of the AutoResetEvent
class is initiated as closed; - The Action delegate
action_pass_counter
increases the count
variable by 1
if the AutoResetEvent
is open; - In the test No.1, the toll booth is closed so the
count
variable remains 0
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, the count = 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 an AutoResetEvent.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 integer initialCount
value. The initialCount
specifies the number of the CountdownEvent.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()
{
CountdownEvent cde = new CountdownEvent(3);
Func<bool> func_is_blocked = () =>
{
bool blocked = true;
if (cde.Wait(2 * 1000)) { blocked = false; }
return blocked;
};
Assert.IsTrue(func_is_blocked());
Action action_issue_signal = () => { cde.Signal(); };
Task.Run(action_issue_signal);
Task.Run(action_issue_signal);
Task.Run(action_issue_signal);
Assert.IsFalse(func_is_blocked());
Assert.IsFalse(func_is_blocked());
bool exceptionThrown = false;
try { cde.Signal(); }
catch (Exception) { exceptionThrown = true; }
Assert.IsTrue(exceptionThrown);
}
}
}
- In the test method
D_CountdownEvent_Test()
, an instance of the CountdownEvent
class is initiated with initialCount = 3
; - The "Func<bool>" delegate
func_is_blocked
returns true
if the door is closed, false
if the door is open; - The Action delegate
action_issue_signal
issues a CountdownEvent.Signal()
to the CountdownEvent
instance; - In the test No.1, the
func_is_blocked
returns true
because the door is closed; - In the test No.2, the
func_is_blocked
returns false
because 3 threads are running to signal the door to open; - In the test No.3, the
func_is_blocked
returns false
. It shows us that if the door is open it remains open, until the CountdownEvent.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 the initialCount
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 a ManualResetEvent
; - When initiated with
EventResetMode.AutoReset
, it behaves exactly like an AutoResetEvent
.
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 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;
};
ewh_manual.Set();
Assert.IsFalse(func_manual_is_blocked());
Assert.IsFalse(func_manual_is_blocked());
ewh_manual.Reset();
Assert.IsTrue(func_manual_is_blocked());
EventWaitHandle ewh_auto
= new EventWaitHandle(false, EventResetMode.AutoReset);
int count = 0;
object tlock = new object();
Action action_auto_pass_counter = () =>
{
if (ewh_auto.WaitOne(2 * 1000))
{
lock (tlock) { count++; }
}
};
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());
Assert.AreEqual(1, count);
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
returns true
if the thread is blocked; - The test No.1 shows that if we initiate the
EventWaitHandle
with EventResetMode.ManualReset
, we need to manually open and close the door; - The Action
action_auto_pass_counter
increase the count variable by 1
if the door is open; - The test No.2 shows that if we initiate the
EventWaitHandle
with EventResetMode.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 the EventWaitHandle.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;
};
Assert.IsTrue(func_mres_is_blocked());
mres.Set();
Assert.IsFalse(func_mres_is_blocked());
Assert.IsFalse(func_mres_is_blocked());
mres.Reset();
Assert.IsTrue(func_mres_is_blocked());
Assert.IsTrue(func_mres_is_blocked());
}
}
}
- The
Func<bool>
func_mres_is_blocked
returns true
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 by ManualResetEventSlim.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 the System.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
, and ManualResetEventSlim
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