Introduction
This article shows how to code a synchronized
statement in C++ that works in a similar way to Java. The target of this code is to make a piece of code like the following, compilable and executable in C++:
synchronized(myMutex)
{
}
The Mutex class
The following piece of code presents a mutex class with lock/unlock semantics (common in many libraries):
class Mutex
{
public:
Mutex()
{
InitializeCriticalSection(&m_criticalSection);
}
~Mutex()
{
DeleteCriticalSection(&m_criticalSection);
}
void lock()
{
EnterCriticalSection(&m_criticalSection);
}
void unlock()
{
LeaveCriticalSection(&m_criticalSection);
}
private:
CRITICAL_SECTION m_criticalSection;
};
There is nothing particularly special for the above class:
- it initializes the critical section upon construction,
- it deletes the critical section,
- the method
lock()
locks the critical section, and
- the method
unlock()
unlocks the critical section.
We are going to use critical sections, but any synchronization primitive applies.
The Lock class
In order to be consistent with the C++ established code practices, we need a special class for implementing the RAII (Resource Acquisition Is Initialisation) pattern. The following piece of code shows such a class:
class Lock
{
public:
Lock(Mutex &mutex) : m_mutex(mutex), m_locked(true)
{
mutex.lock();
}
~Lock()
{
m_mutex.unlock();
}
operator bool () const
{
return m_locked;
}
void setUnlock()
{
m_locked = false;
}
private:
Mutex &m_mutex;
bool m_locked;
};
Things to note for this class:
- it locks the mutex upon construction, and
- it unlocks the mutex upon destruction.
Using the above class is pretty straightforward:
Mutex mutex1;
...
Lock lock1(mutex1);
The "synchronized" macro
The synchronized
statement can be coded as a macro like this:
#define synchronized(M) for(Lock M##_lock = M; M##_lock; M##_lock.setUnlock())
where, the parameter M
is the mutex variable to use for locking.
Example of using the "synchronized" macro
The following piece of code shows how to use the synchronized macro: it coordinates two threads that print the alphabet in the standard output. Without synchronization, the output is not correct:
int thread_count = 0;
Mutex mutex1;
DWORD CALLBACK thread_proc(LPVOID params)
{
for(int i = 0; i < 10; ++i)
{
synchronized(mutex1)
{
for(char c = 'A'; c <= 'Z'; ++c)
{
cout << c;
}
cout << endl;
}
}
thread_count--;
return 0;
}
int main()
{
thread_count = 2;
CreateThread(0, 0, thread_proc, 0, 0, 0);
CreateThread(0, 0, thread_proc, 0, 0, 0);
while (thread_count) Sleep(0);
getchar();
return 0;
}
How it works
The macro exploits the nature of the for
statement of C++ to do the following (in the presented order):
- initialization part: a local lock variable is defined that locks the given mutex; the lock variable contains an internal flag which is set to true.
- test part: the lock variable is tested and found to be true: the code inside the loop is executed.
- increment part: the lock variable's internal flag is set to false.
- test part: the lock variable is tested and found to be false: the loop exits.
- exit part: the lock variable is destroyed, unlocking the mutex.
Advantages over classic RAII
Using this way to code RAII has some advantages over the traditional method:
- it makes the code more readable,
- it helps avoiding declaration of lock variables, and
- it ties the code to be synchronized with the synchronization scope.
Notes
The synchronized
macro is exception-safe, since it unlocks its mutex upon destruction.