Click here to Skip to main content
15,669,133 members
Articles / Programming Languages / C#
Article
Posted 30 Jul 2016

Tagged as

Stats

27.9K views
168 downloads
6 bookmarked

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

Introduction

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 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 threads read 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 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 is 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 openned 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 postings and I hope this note can help you one way or the other.

History

First Revision - 7/30/2016

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 
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 
I think you missed Sacha's point. It's important that when writing async code, you remain "async all the way down". Mixing async code with blocking code is dangerous which can easily result in deadlock, or worse, livelock. So Sacha is recommending you mention the async versions of these synchronization primitives. Of course, only one such type exists in the framework (SemaphoreSlim has methods for waiting asynchronously), the rest are all types you have to create, based off the excellent series of blog posts that Sacha linked to.

Personally, I'm more concerned with yet another article that talks about the Event types without discussing the dangers of these synchronization primitives. The vast majority of time I've seen them used the code has contained race conditions and should have been written using Monitor instead. There's a reason POSIX threads don't have Event types and only include condition variables.
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.