Introduction
Lot of people find it difficult to code for multithreaded applications. When I first started to learn about threads, I was in a similar situation. Understanding threads and their synchronization conceptually is very important. Once the concepts are clear, it is very easy to code.
I will be trying to explain the concepts by using an example. This article assumes that you have basic knowledge of Windows thread APIs (at least how to create thread, pass data to thread function and wait for thread or threads to finish up). If you don’t have the basic knowledge, you can refer to the document Thread.docx in the document package. This document is taken from the web.
Using the Code
Before you start reading further down, please download the package. The package will have 2 folders, namely Files and Documents. The CPP files which end with test have tests for various scenarios. By default, the CriticalSectionTest
will be run. For running other tests, you have to comment out the CriticalSectionTest
and uncomment the Test
which you want to run.
In the Document folder, you will find the Thread.docx which explains thread basic terminology and its usage. I recommend that you read this document first. It will give you an idea on what threads are and what is some basic terminology of threads.
In files folder, you will find all the files which are needed for this article. Create a Visual Studio 9 project and add all the files. Build it. Before you start the build, change the following setting in Visual Studio. Refer to image below:
Go to Project->Properties->General->Character Set.
Change from Use Unicode character Set to Use Multi-Byte Character Set. Refer to the Image below:
In files, you will see cpp files which end with Test (for example MutexTest.cpp). These files contain the tests. The contents of all the files are commented. So if you want to run one of the tests, you have to uncomment the content of that file. You can run 1 test at a time. All the files content except that one which you want to run the test of should be commented.
Assuming you are ready with the project, let’s start to learn.
Let’s take the scenario that there is a bathroom. The people stand in the queue outside the bathroom and they want to use it one by one in an orderly fashion. There has to be an administrator who can monitor them so they use the bathroom one at a time. In order to do so, the administrator can define his own rules to create the order.
We will understand the threads and the synchronization process by deriving the analogy from the above example and we will code it too by using various synchronization techniques.
What is Resource?
As the name indicates, resource can be understood as something which can be used. Here in our case, it is the bathroom. Or in other words, it is whatever lies inside the door.
What are Threads?
Threads can be understood as a series of instructions. Like in our example, the person waiting in the queue is thread.
Synchronization Objects
Let’s say there is no administrator who can create rules on how the persons should use the bathroom. Imagine the chaos created by such a scenario. To maintain the order in the system, we need some kind of rule so that the resources (in our case, the bathroom) are used by the threads (in our case Persons
) in a systematic and orderly way. To maintain the order, the administrator defines some kind of mechanism (here in our case, it's say key and bulb). The mechanism by which the things can be done in an orderly fashion can be understood as Synchronization Objects.
Let us first write the myThread
class:
Check out the myThread.h and myThread.cpp files in the download.
- Constructor sets the class member such as callback function, thread name, Parameters which is passed to thread function.
Execute()
starts the thread and stores the handle of the thread in the class variable m_thread
.
waitForThreadToFinish()
waits for the thread to finish up .
waitForThreadsToFinish(int numOfThreads,HANDLE threadHandle[])
waits for all the threads whose handle we pass in this method in the form of handle array. This is made as static
method because it is waiting for many threads to finish so we should be able to call it in a general way.
- There are methods for the threads to resume, suspend the threads. There are other functions too which are self explanatory.
Different Administrators
Critical Section
Let’s say there is an administrator whose name is in the Critical Section. He has defined his own rules to maintain the order. Let’s see how he is maintaining the order:
- He stands in front of the queue of the persons and allows 1 person to go at a time to the bathroom.
- He only allows the person to go when he sees the bathroom is empty.
Let us write the myCriticalSection
class. Checkout the myCriticalSection.h and myCriticalSection.cpp files in the download packet.
- Constructor initializes the critical section. Initialization symbolizes the creation of the administrator.
- Destructor deletes the critical section. Deletion symbolizes the deletion of administrator. When all the persons in the queue have used the bathroom, there is no need of administrator so deletion is required.
LockCriticalSection()
method is used to lock the resources as someone is using them. This signifies that the administrator has locked the bathroom because someone is using it and he is standing in front to the queue of the persons and not allowing anyone to go inside. UnLockCriticalSection()
method is used to unlock the resource as the thread which was accessing it as it is done with that. This signifies that the person who was using the bathroom is done using that so the administrator unlocks that and allows the first person in the queue to enter. CloseCriticalSection()
deletes the administrator exclusively.
Till this point, we are ready with our Critical Section administrator and the Thread. Let’s try to simulate the bathroom scenario by writing the program.
Have a look at the CriticalSectionTest.cpp file. This has the main function.
- We will create the administrator:
myCriticalSection csBathroom("Bathroom");
- We will create 4 threads, namely (
Ram
, Shyam
, Raj
and Hari
) and run them. This signifies that we have created the 4 persons and they all want to go to bathroom. One thing here is very important that the thing which is responsible for putting all the threads in the queue in your operating system. We will store the handle of the threads in the handle array.
myThread ShyamThread("Shyam", UseBathroomMonitoredByCS, (void*)"Shyam");
ShyamThread.execute();
handles[1] = ShyamThread.getThreadHandle();
- The function which each thread/person tries to run is
UseBathroom()
. Inside this function, the administrator locks the resources and then person/threads use the resources and once the person/thread finishes using the resources, the administrator unlocks the resource. Resource usage is defined by these 4 lines of code.
x = count;
x++;
count = x;
Sleep(5000);
The first 3 lines are just incrementing the count. It is done in a strange way though. For now, just assume that it works that way. There will be an explanation at the end of this tutorial. We have put sleep for 5 second so that it can simulate the usage of bathroom by the person.
- Finally, the system waits for all the threads to finish. It symbolizes that all the persons have used the bathroom.
WaitForMultipleObjects(numThreads,handles,TRUE,INFINITE);
- At last, we delete the administrator as his job is done.
csBathroom.CloseCriticalSection();
- Now, let’s run and see.
Initializing the critical section
Entering the critical section
Ram is in the bathroom
Ram is using the bathroom
Ram will leave the bathroom
Leaving the critical section Entering the critical section
Shyam is in the bathroom
Shyam is using the bathroom
Shyam will leave the bathroom
Leaving the critical section Entering the critical section
Raj is in the bathroom
Raj is using the bathroom
Raj will leave the bathroom
Leaving the critical section Entering the critical section
Hari is in the bathroom
Hari is using the bathroom
Hari will leave the bathroom
Leaving the critical section
Deleting the critical section
The no. of people used the bathroom are:4
Deleting the critical section
As we can see, there is order in the system. It is possible that you run next time and the first person to use the bathroom can be Shyam
. The queuing of threads is done by the operating system and hence it can be random. We don’t have any control on that.
Mutex
Let’s say there is an administrator whose name in Mutex. Mr. Mutex has defined his own rules to maintain the order. Let’s see how he is maintaining the order:
- He has a key to the bathroom. He gives the key to the person who is standing first in the row. As soon as the person/thread gets the key, he basically owns the mutex or the key.
- The person/thread uses the bathroom/resources and then gives the key back/releases the mutex to the administrator.
- The administrator gives this key again to the first person in the queue.
- The Mutex is not so smart and has a very bad memory. So he keeps a register with him and always writes whether or not the key is with him. If he decided to give the key to person he marks false in the register and then gives it to the first person waiting in the queue. The marking of false can be seen as the signal to the person that key is available for grab. When the key is with him, he marks TRUE in register .
Let us write the myMutex
class. Checkout the myMutex.h and myMutex.cpp files in the download packet.
- Constructor initializes the mutex. Initialization symbolizes the creation of the administrator.
- It is done by system method
CreateMutex()
. This method takes 3 parameters:
- Security attributes
- If this value is TRUE and the caller created the mutex, the calling thread obtains initial ownership of the mutex object. Otherwise, the calling thread does not obtain ownership of the mutex. Set to TRUE if you want to initially own the mutex. If set to FALSE, the first thread which asks for permission will get the ownership.
- The name of the mutex, or 0 if nameless.
o Return Values:
- If the function succeeds, the return value is a handle to the newly created mutex object.
- If the function fails, the return value is
NULL
. To get extended error information, call GetLastError
. - If the mutex is a named mutex and the object existed before this function call, the return value is a handle to the existing object,
GetLastError
returns ERROR_ALREADY_EXISTS
, bInitialOwner
is ignored, and the calling thread is not granted ownership. However, if the caller
has limited access rights, the function will fail with ERROR_ACCESS_DENIED
and the caller should use the OpenMutex
function.
o If the 2nd parameter is false
, this means the mutex is in the signaled state and first person in the line can grab it.
- Destructor deletes the mutex. Deletion symbolizes the deletion of administrator. When all the persons in the queue have used the bathroom, there is no need of administrator so deletion is required.
LockMutex()
method is used to lock the resources as someone is using them. This signifies that the administrator has locked the bathroom because someone is using it and he is standing in front to the queue of the persons and not allowing anyone to go inside. This is done by WaitForSingleObject()
with infinite time. WaitForSingleObject()
waits for the handle (in this case, this is mutex handle) and acquires it. UnlockMutex()
method is used to unlock the resource as the thread which was accessing it is as it is done with that. This signifies that the person who was using the bathroom is done using that so administrator unlocks that and allows the first person in the queue to enter. This is done by ReleaseMutex()
method. ReleaseMutex()
releases the mutex. CloseMutex()
deletes the administrator exclusively. - The
OpenMutex
function enables multiple processes to open handles of the same mutex object. The function succeeds only if some process has already created the mutex by using the CreateMutex
function. The calling process can use the returned handle in any function that requires a handle to a mutex object, such as the wait functions, subject to the limitations of the access specified in the first parameter of the function.
o It takes 3 parameters:
- The access to the mutex object. Only the
SYNCHRONIZE
access right is required to use a mutex; to change the mutex's security, specify MUTEX_ALL_ACCESS
. The function fails if the security descriptor of the specified object does not permit the requested access for the calling process. For a list of access rights, see Synchronization Object Security and Access Rights. - If this value is
TRUE
, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle. - The name of the mutex to be opened. Name comparisons are case sensitive.
o Return Values:
- If the function succeeds, the return value is a handle to the mutex object.
- If the function fails, the return value is
NULL
. To get extended error information, call GetLastError
. - If a named mutex does not exist, the function fails and
GetLastError
returns ERROR_FILE_NOT_FOUND
.
We are ready with our Mutex administrator. Let’s try to simulate the bathroom scenario by writing the program.
Have a look at the MutexTest.cpp file. This has the main function. The code is self explanatory as we have done the explanation for Critical Section. The same kind of output is expected.
Semaphore
So now it’s time to get introduced to a new administrator whose name is Semaphore. He has defined the following rules:
- He has a key to the bathroom. He gives the key to the person who is standing first in the row. As soon as the person/thread gets the key, he basically owns the semaphore or the key.
- The person/thread uses the bathroom/resources and then gives the key back/releases the semaphore to the administrator.
- The administrator gives this key again to the first person in the queue.
- The Semaphore is smarter than Mutex. He keeps the count of the keys which is with the Semaphore. So when he is having the key, he increases the counter by
1
. He has a counter on his table which indicates the count. When he gives the key to the person, he decreases the count by 1
. When the counter is showing more than 0, it means the semaphore is up for the grab and the first person in the queue can grab it. - Let us consider a scenario where there are more than 1 bathrooms (let’s say 3) and we have to create the order for the usage of multiple bathrooms. In this scenario, our semaphore administrator can manage it by following its rules:
- a. The initial count in the counter will be showing 3.
- b. He gives one key to the 1st person in the queue and decreases the counter by 1. So when 3 people are in the bathrooms the counter is 0, hence others have to wait.
- c. As soon as any person comes out and hands over the key, the counter gets increased and the first person can take the key and use the bathroom.
- Till now, you will have figured out that if Semaphore can handle only 1 key, it is behaving like Mutex.
- Semaphores can be divided into 2 categories:
- Mutex Semaphores/Binary semaphores.
- If the value of counter can be only 0 and 1, it is called binary semaphores
- If we replace the counter with the Boolean variable, it becomes mutex semaphores.
- Both can be used interchangeably.
- Counter Semaphores
- The maximum value of the counter can be more than 1 as we discussed above.
Let us write the mySemaphore
class. Checkout the mySemaphore.h and mySemaphore.cpp files in the download packet.
We will write this class with bit more details to it. Like we will initialize the security attributes. We will write the constructor which can create and open up the semaphores. Memory allocation/deletion is done through HeapAlloc()
and HeapFree()
method. This will give the real feel of what the actual class should look like.
• Constructor initializes the semaphores and if it already exists, it tries to open it. Initialization symbolizes the creation of the administrator. It can open up the semaphore if it already exists.
- This is done by method
CreateSemaphore()
. It creates or open the semaphore - It takes 4 arguments:
- Security attributes.
- The initial value of the semaphore count. The initial value here given is 1 - it means that the semaphore is signalled and first thread in the queue.
- The maximum value of the semaphore count.
- The name of the semaphore, or 0 if the semaphore is nameless.
- Return Values: Returns a handle to the semaphore, or
NULL
on error. If a semaphore of the specified name already exists, then GetLastError()
will return ERROR_ALREADY_EXISTS
but the handle will be valid.
- Opening of existing semaphores is done by
OpenSemaphore()
. Details can be found on the web.
- Destructor deletes the semaphores. Deletion symbolizes the deletion of administrator. When all the persons in the queue have used the bathroom, there is no need of administrator so deletion is required.
LockSemaphore (BOOL wait=TRUE)
method is used to lock the resources as someone is using them. This signifies that the administrator has locked the bathroom because someone is using it and he is standing in front to the queue of the persons and not allowing anyone to go inside. It takes a Boolean argument. If this is true
, it waits for infinite time and in case of false
, it does not wait for any time. This is done by WaitForSingleObject()
with infinite time. WaitForSingleObject()
waits for the handle (in this case, this is semaphore handle) and acquires it.
UnlockSemaphore ()
method is used to unlock the resource as the thread which was accessing it as it is done with that. This signifies that the person who was using the bathroom is done using that so administrator unlocks that and allows the first person in the queue to enter. This is done by ReleaseSemaphore ()
method. ReleaseSemaphore ()
releases the semaphore. CloseSemaphore ()
deletes the administrator exclusively. - Other methods are fairly straightforward to understand.
We are ready with our Semaphore administrator. Let’s try to simulate the bathroom scenario by writing the program.
Have a look at the SemaphoreTest.cpp file. This has the main function. The code is self explanatory as we have done the explanation for Critical Section. The same kind of output is expected. If you have understood the concepts of semaphore, try creating the program for 3 bathroom and 10 people.
Event
Let us learn about a new administrator called Event
. He has defined the following rules:
- He has put a bulb on the top of bathroom door. It will act as a signal for the persons standing the queue.
- Once a person is inside the bathroom, the bulb is red saying to others to wait.
- When the person comes out of the bathroom, the signal becomes green indicating to the persons in the queue that the resource is up for grabs.
- Event administrator is smart and somewhat lazy so he asks threads to do the signaling (turning red to green when they are done using the bathroom) and he sits happily watching them.
- If he delegates the task of turning the bulb from green to red to the threads this way, it is called manual signaling. Otherwise, if the administrator is doing it, it is called automatic signaling.
- Manual Reset Events:
- As we know, there are no free lunches in the world, hence when the threads are handling signaling (green to red only) themselves, all the threads in the queue try to grab the bathroom as soon as the bulb turn green.
- The thread has the responsibility to turn the signal red to green and green to red. Green to red is done by
resetEvent()
and red to green is done by setEvent()
.
- Auto Reset Events:
- As soon as the bulb turn from red to green, only 1 thread in the queue (1st one) tries to grab the resource.
- The thread has the responsibility to turn the signal red to green. Green to red is done by
resetEvent()
and red to green is done by setEvent()
.
- Auto reset event is useful when one thread has to coordinate with one another thread. Manual reset event will be useful when one thread needs to coordinate with any number of threads.
- Let us write the
myEvent
class. Checkout the myEvent.h and myEvent.cpp files in the download packet.
- Constructor initializes the event. It takes three parameters. First one is event name, second is Boolean to indicate whether the event is manual reset or auto reset. Third is for the initial signal state.
- This is done by
CreateEvent()
. Creates an event, or opens one if the name specified already exists. - It takes 4 arguments:
- Security attributes
TRUE
for manual reset, FALSE
for automatic reset. - The initial state of the event:
TRUE
if signaled. - The name of the event, or
0
if nameless.
- Return values: Returns a handle to the event, or
0
on error. If an event of the specified name already exists, then GetLastError
will return ERROR_ALREADY_EXISTS
but the handle will be valid.
- Destructor deletes the event. Deletion symbolizes the deletion of administrator. When all the persons in the queue have used the bathroom, there is no need of administrator so deletion is required. This is done by
closehandle()
message
setEvent ()
method is used to make signal from red to green. resetEvent ()
method is used to make signal from green to red. pulseEvent()
method details can be found on web. closeEvent ()
deletes the administrator exclusively. - Other methods are fairly straightforward to understand.
We are ready with our Event administrator. Let’s try to simulate the bathroom scenario by writing the program.
- We will try to simulate the bathroom scenario by using auto reset events. Have a look at AutoEventTest.cpp. The output is as expected and each person uses the bathroom one by one.
- Let’s see what the method
UseBathroomMonitoredByAutoEvent
does.
- First, we wait for the signal by using
WaitForEvent();
- As soon as the signal is green, the 1st thread in the queue will try to grab the resource (it is explained earlier).
- Use the resources:
Use Resources
x = count;
x++;
count = x;
Sleep(5000);
- Set the signal to green, so other can be used.
- We will see the synchronized use of the bathroom here.
- Since initial state is red or non signaled, we change it to green or signaled in main
manualEventBathroom.setEvent();
- Other things are kept the same as for earlier examples.
- This type of auto reset event can be used when one thread has to signal only 1 thread. That is why this is helping us in synchronizing.
We will try to simulate the bathroom scenario by using manual reset events. Have a look at ManualEventTest.cpp. The output which we get is something like this:
Initializing the Event
Giving the signal
The signal has been given, Acquire the resource The signal has been given,
Acquire the resource The signal has been given,
Acquire the resource The signal has been given, Acquire the resource
Resetting the Event Resetting the Event Resetting the Event Resetting the Event
RamShyamRajHari is in the bathroom is in the bathroom is in the bathroom is in the bathroom
RamShyamRajHari is using the bathroom is using the
bathroom is using the bathroom is using the bathroom
RamShyamRajHari will leave the bathroom will leave the
bathroom will leave the bathroom will leave the bathroom
Giving the signal Giving the signal Giving the signal Giving the signal
Deleting the Event
The no. of people used the bathroom are:4
Deleting the Event
- Let’s see what the method
UseBathroomMonitoredByManualEvent
does.
- First, we wait for the signal by using
WaitForEvent()
; - As soon as the signal is green, all threads in the queue will try to grab the resource (it is explained earlier).
- As soon as we get the signal, we make the signal to red:
manualEventBathroom.resetEvent();
- Use the resources
Use Resources
x = count;
x++;
count = x;
Sleep(5000);
- Set the signal to green, so other can be used.
- We are not seeing the synchronized usage. It very random.
- Now the question arises in what condition we should use the manual reset event. This can be used when one thread wants to signal many other threads. In this case, all the threads/persons are catching the signal and trying to grab the bathroom.
- Since initial state is red or non signaled, we change it to green or signaled in main.
manualEventBathroom.setEvent();
- Other things are kept same as for earlier examples.
Till now, you would have figured out that events are different from locks (critical section, mutex and semaphores). Events are used for signaling. We however use to synchronize the threads by using correct mechanism. As we have seen, auto reset events can be used to synchronize the threads. However still we have to get the clarity on how to use the manual reset events.
Let us see now how we can use the manual reset events. We will understand this scenario by solving a problem. Here is the problem:
Assume there is a race going on.
By looking at the output, we have simulated the scenario which we have mentioned in the problem statement above.
Let us try to write an event
class which is smarter than the event
class which we have written. Let us call this mySmartEvent
class. Have a look at mySmartEvent.h and mySmartEvent.cpp file.
What is smart about this event class?
Whenever the object of this class is created, an event is created and we start a thread which monitors that event state. As the event gets signaled, this thread queues up a callback for the execution. Let us try to see the class functions and their implementations.
- Constructor takes three parameters. First one is the event name; second one is the callback function name and last is a Boolean which governs whether or not the callback should be queued.
- The manual reset event is created whose initial state is not signaled.
- The callback function is set to a class member variable which will be used afterwards.
- A thread is started which will call the method
ThreadProc()
. The interesting thing to notice is that we pass the this pointer to the thread arguments in here. This is done because we will require the event object in the Thread Proc.
ThreadProc
waits for the event object to be signaled state.
- This is done by
WaitForSingleObjectEx()
.
- The callback function is queued by using
QueueUserAPC()
. - We have made the thread sleep infinitely to make sure the callback gets called.
- Other functions are the same as in earlier
myEvent
class.
Now let us try to test this class. Have a look at SmartEventTest.cpp.
What is the difference between WaitForSingleObjectEx and WaitForSingleObject?
The function WaitForSingleObjectEx
is similar to the WaitForSingleObject
function except that it takes an additional boolean parameter whose value decides whether the calling thread will be in an alertable wait state or not.
If this boolean parameter is set to TRUE
, the function will return whenever a completion routine or asynchronous procedure call (APC) is queued. An APC is queued when the QueueUserAPC
API is called and a completion routine queuing happens when the ReadFileEx
or WriteFileEx
APIs are completed. When the ReadFileEx
or WriteFileEx
APIs are called, a completion function name is passed as parameter. When the actual read or write completes, the wait function returns and the completion function will be called by the system, provided the thread is in an alertable wait state.
Let’s think of an example to better understand what the alertable wait state means. Assume you are doing inter-process communication (IPC) using named pipes or mailslots. Further assume that the client application has to wait for a thread to complete. At the same time, whenever a message arrives at the pipe, some operations have to be performed. This is possible by using the WaitForSingleObjectEx
API and giving the handle of the thread as the first parameter and setting the alertable parameter to TRUE
.
Summary
With this, I will be finishing up this article. Just to recap, we have learn about the basic synchronization objects(Critical Section, Mutex and Semaphores). We have also learn about Events. We have seen how the events are different from locks and how they can be used to synchronize the threads. We have also seen how to create a Smart Event class which will be having a callback function called if specified.
Please forgive my grammatical mistakes. Your suggestions, constructive comments are appreciated.
Points of Interest
Threads are not so difficult to deal with. The concepts of threads are more or less the same everywhere across all operation systems. Learning basic concepts always saves a lot of time afterwards.