Introduction
Usually when we want to use a variable in multiple threads simultaneously we put some synchronization object nearby it, lock it before accessing that variable and unlock it after that.
This opens a way to a number of hard-to-find bugs.
- We can just forget to make a lock on the synchronization object before accessing the variable which can lead to data corruption.
- If we forget to unlock the synchronization object after the variable is used, then another thread can be blocked forever while waiting for that synchronization object.
- Passing the reference or a pointer to such a variable into an existing function as a parameter without its synchronization object will allow for data corruption because this variable can be accessed unguarded.
Example
The following example demonstrates the first of the above situations which can lead to data corruption (if we forget to make a lock):
#include<string>
class CMyClass
{
public:
. . . std::string m_data;
HANDLE m_mutex;
};
DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
CMyClass* myClass = (CMyClass*)lpParameter;
myClass->m_data.assign("another thread");
return 0;
}
int main()
{
CMyClass myClass;
HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);
::WaitForSingleObject(m_mutex, INFINITE);
myClass.m_data.assign("main thread");
::ReleaseMutex(mutex);
::CloseHandle(threadA);
}
In this example m_data
member of object myClass
can be corrupted because the main thread has locked the m_mutex
before accessing m_data
but another thread has not used m_mutex
at all before making its own attempt to change the value of m_data
. If these threads try to change the value of m_data
at the same time then m_data
can be corrupted. To avoid this situation, access to m_data
in both threads must be synchronized using the same synchronization object.
Solution
The solution to this problem is an OOP approach to bind the synchronization object to the data which we want to be thread-safe, hide them both inside a thread-safe smart pointer and use this thread-safe smart pointer instead of the raw data which it encapsulates.
Features of the New Thread-safe Smart Pointer
- It can be used for thread synchronization because it contains its own synchronization object (It does not require any synchronization object around it)
- It is based on the Boost library (it keeps data using
boost::shared_ptr
) - It is a thread-safe smart pointer for Windows because mutexes inside it are created using WinAPI (If anybody can figure out how to replace WinAPI mutexes with some of the Boost mutexes, please, drop a message in the forum below.)
- It is a smart pointer because:
- it allows a synchronized access to its data through the operator-> and operator*
- it deletes the data which it contains
- it can be passed by value
It Consists of Two Classes
CThreadPtr
– Main class containing pointer to the data of any type, a mutex for the pointer access synchronization and another mutex to protect its own state.CThreadPtr::CLocker
– Nested class providing lock/unlock operations on the CThreadPtr
’s mutexes. The lock is done in the CLocker
's constructor and unlock – in its destructor.
How To Use It
Let’s look at the following code which is the example above refactored to use the thread-safe smart pointer:
#include<string>
#include "ThreadPtr.hpp" // for the thread-safe smart pointer
typedef CThreadPtr<std::string> TMyDataPtr;
class CMyClass
{
public:
CMyClass()
: m_data(new std::string)
{. . .}
. . .
TMyDataPtr m_data;
};
DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
CMyClass* myClass = (CMyClass*)lpParameter;
myClass->m_data->assign("another thread");
return 0;
}
int main()
{
CMyClass myClass;
HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);
myClass.m_data->assign("main thread");
::CloseHandle(threadA);
}
In this example, we just cannot forget to lock m_data
before using it.
Transactional Lock
Transactional lock allows to make changes to several variables without letting other threads access any of them until the end of the transaction. This is usually needed to keep those variables consistent to each other.
Transactional lock can be implemented by using CLocker
(CThreadPtr
's nested class) as in the following example:
typedef CThreadPtr<MyStructWithSeveralDataMembers> TMyDataPtr;
void MakeTransaction( TMyDataPtr& obj)
{
...
{
TMyDataPtr::CLocker lock(obj);
...
obj->ChangeOneDataMember();
obj->ChangeAnotherDataMember();
...
} ...
}
In this example, variable lock
will be destroyed (and the mutex released) at the exit from the block where it was defined thus allowing to execute any number of operations on the obj
in the thread-safe way.
operator*()
To get a reference to the data which an object of CThreadPtr
points to, we can use the following line of code:
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr = new std::string;
...
{
TMyDataPtr::CLocker lock(ptr);
...
std::string& ref = **ptr;
ref = "new value";
}
To get a pointer to the data which an object of CThreadPtr
points to, we can use the following line of code:
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr = new std::string;
...
{
TMyDataPtr::CLocker lock(ptr);
...
std::string* rawPtr = **ptr;
rawPtr->assign("new value");
}
Use Operator bool () Carefully
In the following piece of code CThreadPtr
's operator bool ()
is used to make sure ptr
has been initialized to avoid access violation:
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr;
...
if (ptr) ptr->assign("some value");
But any way the exception can occur in this code. It can happen if another thread calls method reset
on the same object ptr
...
ptr->reset();
... when the first thread was between line A and line B. To avoid such a data race condition, the transactional lock (described above) should be used around both transactional lock line A and line B. But if you do not intend to apply method reset
to object ptr
wherever in your code then using the transactional lock will just decrease the performance of your code and its readability.
Now with this thread-safe smart pointer, you can use any new object (not an existing one) of any type in a multithreaded environment.
Further Readings
You can try to modify this smart pointer so that it could contain a pointer to a COM interface by replacing boost::shared_ptr
inside CThreadPtr
with CComPtr
. Otherwise you could look into this site some time later for another of my article devoted to a COM thread-safe smart pointer.