Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Thread Synchronization Lock ManualResetEvent AutoResetEvent CountdownEvent and More

Rate me:
Please Sign up or sign in to vote.
4.79/5 (9 votes)
30 Jul 2016CPOL9 min read 30.9K   171   6   13
Thread synchronization mechanisms and few other classes in the "System.Threading" namespace
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.

Image 1

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.

C#
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" 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.
C#
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 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.
C#
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 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.
C#
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 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.
C#
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 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.

C#
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 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.

Image 2

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I have been working in the IT industry for some time. It is still exciting and I am still learning. I am a happy and honest person, and I want to be your friend.

Comments and Discussions

 
Questionthread.sleep(); Pin
AnotherKen1-Aug-16 12:57
professionalAnotherKen1-Aug-16 12:57 
An interesting article, thank you for sharing your knowledge of thread control. One thing I would like to add is that thread.sleep() may seem like an inept use of thread synchronization, but you must also consider that your programs are likely operating in an environment where they are competing with other programs for system resources, therefore, placing thread.sleep() commands in convenient places within your code can make it very responsible with it's use of shared system resources.
AnswerRe: thread.sleep(); Pin
Dr. Song Li1-Aug-16 15:05
Dr. Song Li1-Aug-16 15:05 
SuggestionRecommendation - include Monitor instead of lock Pin
PureNsanity1-Aug-16 4:23
professionalPureNsanity1-Aug-16 4:23 
GeneralRe: Recommendation - include Monitor instead of lock Pin
Dr. Song Li1-Aug-16 4:27
Dr. Song Li1-Aug-16 4:27 
QuestionAsync await Pin
Sacha Barber31-Jul-16 11:23
Sacha Barber31-Jul-16 11:23 
AnswerRe: Async await Pin
Dr. Song Li31-Jul-16 11:28
Dr. Song Li31-Jul-16 11:28 
GeneralRe: Async await Pin
wkempf1-Aug-16 2:33
wkempf1-Aug-16 2:33 
GeneralRe: Async await Pin
Dr. Song Li1-Aug-16 3:04
Dr. Song Li1-Aug-16 3:04 
GeneralRe: Async await Pin
Sacha Barber1-Aug-16 5:35
Sacha Barber1-Aug-16 5:35 
AnswerRe: Async await Pin
Dr. Song Li31-Jul-16 11:30
Dr. Song Li31-Jul-16 11:30 
GeneralRe: Async await Pin
PureNsanity1-Aug-16 4:16
professionalPureNsanity1-Aug-16 4:16 
GeneralRe: Async await Pin
Dr. Song Li1-Aug-16 4:20
Dr. Song Li1-Aug-16 4:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.