Introduction
VC++ does not have an easy to use synchronized block like that of Java. But it is easier to simulate a macro which will work exactly like the synchronized block of Java. MFC provides classes for synchronization of threads. They are CSyncObject
, CCriticalSection
, CMutex
, CEvent
, CSemaphore
, CSingleLock
and CMultiLock
. CSyncObject
is the base class for all the other synchronization classes. All the classes implement the two important functions of CSyncObject
viz. Lock()
and Unlock()
.
Who can read this article
Dear reader, you should know how to create threads using MFC (yes using AfxBeginThread
or CWinThread
directly)
Why synchronization
If two threads try to access the same data it may cause a fatal error. For example one thread may try to write into a file, while at the same time some other thread may try to read the data which was never written!! Only after the file is written, it should allow the other thread to read through some inter-thread communication. So to avoid the simultaneous access of data or parts of code, the threads should be protected by a mechanism, which allows only one thread to access the part of code or data. The data or code which is subject to multiple threads simultaneously is called a Critical Section.
What this article tries to do?
This article tries to simulate a synchronized block. The code or data inside the synchronized block ensures that only a single thread can access. The block also provides a way to wait till a flag is set by another thread which is termed as thread communication. Automatic release of the lock gained is ensured by the synchronized block using a technique called Resource Acquisition Is Initialization. The lock is released even if an exception is raised in the middle of the block.
What this article does not try to explain?
This article does not try to explain creation of threads. It just assumes you know how to create threads and face a problem in synchronization. It does not try to explain about deadlocks.
How MFC provides synchronization
A MFC CCriticalSection
provides an encapsulation way to protect the critical sections of code or data. A thread before entering (accessing) into the critical section calls the Lock()
member function on CCriticalSection
, does the delicate process and leaves (releases) the critical section by calling Unlock()
member function. If already the the CCriticalSection
is locked by another thread say thread2, then the thread calling Lock()
say thread1 will wait (without occupying the CPU cycles) until thread2 finishes its work on critical section and calls Unlock()
. Then the thread1 is made eligible for entering the critical section.
Advantages of using SYNCHRONIZED block
Using MFC synchronization block we can synchronize code from corruption by multiple threads. But we may try to gain a lock and may not release the lock properly or we may try to access the critical sections of code without gaining a lock. The code which we may use to synchronize may not be legible. Using synchronized block is legible. Further it ensures that the lock is released properly even under abnormal exceptions using Resource Acquisition Is Initialization. The real advantage of synchronized block is it makes thread communication easier by using a function called Wait
!!
Use of Resource Acquisition Is Initialization to release the Lock properly
Let us study the following example:
void some_func()
{
char *arr = new char[1024 * 5];
delete[] arr;
}
In the above sample code we saw how in case of an exception the memory will not be released properly which will result in memory leak. If there are no failures there will be no memory leaks. But there is a way by which the memory will get released in spite of exceptions. This can be achieved using a wrapper often called a smart pointer class which will take care of releasing the resource (memory, file, socket, handles ...) properly. See the following code:
class CSmartCharArray
{
private:
char *ptr;
public:
CSmartCharArray(char *addr) : ptr(offset) {}
operator char*() { return ptr; }
virtual ~CSmartCharArray()
{
delete ptr;
ptr = 0;
}
};
...
...
...
void some_func()
{
CSmartCharArray arr(new char[1024 * 5]);
}
In the above code the variable arr
is declared inside the function some_func()
and it's scope will be over when the control from the function is transferred (normally or abnormally). So it's destructor will always be called which will release the array created on the heap!!
To release the Lock()
at the end of the function call in case of failure due to exceptions, the CSyncObject
is usually wrapped using a CSingleLock
object. The CSingleLock
object is created on the stack (not a pointer). The destructor of CSingleLock
gets invoked when stack is unwound always (because of normal end of the block scope or due to some exception). The destructor of CSingleLock
calls the Unlock()
method on its wrapped sync object and thus always the lock is released (See Resource Acquisition Is Initialization). To make use of this technique we create a simple class derived from CSingleLock
called CSynchronizer
.
Simulating SYNCHRONIZED
#ifndef SYNCHRONIZED_BLOCK_MFC
#define SYNCHRONIZED_BLOCK_MFC
#include <afxmt.h>
#define SYNCHRONIZED(slock, cs) if(CSynchronizer slock = CSynchronizer(cs))
class CSynchronizer : public CSingleLock
{
public:
CSynchronizer(CSyncObject *syncobj) : CSingleLock(syncobj, TRUE) {}
operator BOOL() { return CSingleLock::IsLocked(); }
BOOL Wait(DWORD dwTimeOut = INFINITE);
BOOL Notify();
virtual ~CSynchronizer() {}
};
#endif
#include "Synchronizer.h"
BOOL CSynchronizer::Wait(DWORD dwTimeOut)
{
if(CSingleLock::IsLocked())
{
CSingleLock::Unlock();
}
return CSingleLock::Lock(dwTimeOut);
}
BOOL CSynchronizer::Notify()
{
if(CSingleLock::IsLocked())
{
return CSingleLock::Unlock();
}
return TRUE;
}
The CSynchronizer
class tries to gain a Lock on the syncobj
when constructed (locks initially). The class also defines a conversion operator BOOL
. If the CSynchronizer
object has gained a lock, the operator BOOL
returns TRUE
else it returns FALSE
(that is the object is not at all constructed, if the lock is not gained initially). It also contains two functions called Wait
and Notify
(yes very similar to Java's wait and notify). Wait
function releases the owned Lock
and tries to gain it again. It returns TRUE
after successful gain of Lock
. It also takes care to Unlock
if it has already owned a Lock
. Notify
function releases the owned Lock
. It returns TRUE
if the Lock
is already released.
SYNCHRONIZED macro
Next we come to the most important thing in the code, the SYNCHRONIZED
macro. Before studying about how this macro works, we shall see about the scope of objects declared in if
condition. For example see below:
if(int i = 10)
{
}
else
{
}
The variable declared in the if
condition is valid in C++. It can be used in if
block or else
block. The scope of variables declared in if
block goes at the end of if
block. So we cannot access the variables declared in if
after the if
block. So we are going to create CSynchronizer
object in the if
block (which will eventually go out of scope at the end of if
block). But to make the if
allow object creation, the object has to return some boolean value (any integer). That is why our class has an operator BOOL
conversion. We create an object and store it in the variable in if
, which can be used throughout the if
(got the idea).
For example:
if(CSynchronizer syn = CSynchronizer(&some_critical_section))
{
}
The above template code shows a simple synchronized block. The CSynchronizer
is created on some critical section (or some other sync object) and is stored in variable syn
. The variable syn
returns TRUE
if the lock is gained or waits till the lock is gained on critical section. The lock is released on critical section automatically at the end of if
block because the scope of variable declared in if
is over!!! How is the lock released?? Because CSynchronizer
is a derived class of CSingleLock
whose destructor takes care of releasing the lock on destruction. Since CSynchronizer
object gets unwound from stack, the lock is released by the base class CSingleLock
. What about the Wait
and Notify
functions. For example if we have gained a lock using CSynchronizer
and we want to wait for a flag to be set by another thread, we have to release the Lock
temporarily and allow the other thread to set the flag after doing some critical section of code. Then we have to gain the Lock
again and check if the flag is set. This will go in a loop till the flag is set by some other thread which can be coded as follows:
if(CSynchronizer syn = CSynchronizer(&some_critical_section))
{
while(some_flag_is_not_set)
{
syn.Wait();
}
}
The above if
condition is summarized in the small macro as follows:
#define SYNCHRONIZED(slock, cs) if(CSynchronizer slock = CSynchronizer(cs))
where the first parameter slock
is the name of the identifier of synchronizer inside the synchronized block and cs
is the address of some critical section or any other sync object.
So the same wait template code inside a synchronized
can be rewritten as:
SYNCRHONIZED(syn, &some_critical_section)
{
while(some_flag_is_not_set)
{
syn.Wait();
}
}
The use of Notify
is not needed since all threads are notified automatically when synchronizer goes out of scope after if
block. But to make feel a complete code, I have just added Notify
. To use the SYNCHRONIZED
in your MFC applications copy the Synchronizer.h and Synchronizer.cpp code into your MFC application as class CSynchronizer
and use it by including Synchronizer.h.
Enjoy easy synchronization
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.