Click here to Skip to main content
15,860,844 members
Articles / Programming Languages / C

Thread-safe Smart Pointer

Rate me:
Please Sign up or sign in to vote.
3.46/5 (8 votes)
14 Jul 2009CPOL4 min read 49.7K   364   36   7
With this thread-safe smart pointer, you can use an object of any type in a multithreaded environment.

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):

C++
/////////////////////////////////////////////////
#include<string>

class CMyClass
{
public:
. . . // constructor, destructor and other things
    std::string   m_data;
    HANDLE        m_mutex;	
};

DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
    CMyClass* myClass = (CMyClass*)lpParameter;

    // accessing without a prior lock
    myClass->m_data.assign("another thread");
    return 0;
}

int main()
{
    CMyClass    myClass;

    // create another thread
    HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);

    // locking
    ::WaitForSingleObject(m_mutex, INFINITE);

    // accessing
    myClass.m_data.assign("main thread");

    // cleanup
    ::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:

C++
/////////////////////////////////////////////////
#include<string>
#include "ThreadPtr.hpp"        // for the thread-safe smart pointer
typedef CThreadPtr<std::string>   TMyDataPtr;

class CMyClass
{
public:
    // constructor
    CMyClass()
    : m_data(new std::string)
    {. . .}

    . . . // destructor and other things

    TMyDataPtr   m_data;
};

DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
    CMyClass* myClass = (CMyClass*)lpParameter;

    // locking and accessing
    myClass->m_data->assign("another thread");
    return 0;
}

int main()
{
    CMyClass    myClass;

    // create another thread
    HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);

    // locking and accessing
    myClass.m_data->assign("main thread");

    // cleanup
    ::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:

C++
typedef CThreadPtr<MyStructWithSeveralDataMembers>   TMyDataPtr;

void MakeTransaction( TMyDataPtr& obj)
{
    ...
    {
        TMyDataPtr::CLocker lock(obj);
        ...
        obj->ChangeOneDataMember();
        obj->ChangeAnotherDataMember();
        ...
    }// here the mutex is released in the lock's destructor
    ...
}

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:

C++
typedef CThreadPtr<std::string>    TMyDataPtr;

TMyDataPtr ptr = new std::string;
...
{
    TMyDataPtr::CLocker lock(ptr);
    ...
    std::string& ref = **ptr;
    // ref is no more protected by a mutex so variable lock above is necessary
    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:

C++
typedef CThreadPtr<std::string>    TMyDataPtr;

TMyDataPtr ptr = new std::string;
...
{
    TMyDataPtr::CLocker lock(ptr);
    ...
    std::string* rawPtr = **ptr;
    // rawPtr is no more protected by a mutex so variable lock above is necessary
    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:

C++
typedef CThreadPtr<std::string>    TMyDataPtr;

TMyDataPtr ptr;
...
// this is a use of operator bool ()
if (ptr)                                   // line A
    ptr->assign("some value");             // line B

But any way the exception can occur in this code. It can happen if another thread calls method reset on the same object ptr...

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

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
Oleg Fedchenko is a professional in the field of multicore optimization and multicore development for Windows.
Please, visit this Web site http://MulticoreWinSoft.com

Comments and Discussions

 
GeneralNice work. One word of warning though... Pin
wtwhite19-Jan-09 8:36
wtwhite19-Jan-09 8:36 
GeneralRe: Nice work. One word of warning though... Pin
Oleg Fedchenko23-Jan-09 5:17
Oleg Fedchenko23-Jan-09 5:17 
GeneralRe: Nice work. One word of warning though... Pin
wtwhite26-Jan-09 17:14
wtwhite26-Jan-09 17:14 
GeneralRe: Nice work. One word of warning though... Pin
Oleg Fedchenko26-Jan-09 23:20
Oleg Fedchenko26-Jan-09 23:20 
GeneralYou're right. Pin
wtwhite28-Jan-09 15:41
wtwhite28-Jan-09 15:41 
GeneralGood point Pin
Oleg Fedchenko30-Jan-09 0:01
Oleg Fedchenko30-Jan-09 0:01 
GeneralRe: You're right. Pin
Oleg Fedchenko30-Jan-09 3:49
Oleg Fedchenko30-Jan-09 3:49 
Very interesting article by B.Stroustrup. To my opinion he suggested the most common implementation of an object wrapper idea. But it requires additional code to specify the prefix/suffix pair outside the wrapper class.

Now I would say that CThreadPtr is a notationally convenient implementation of a concurrent access control wrapper which also includes features of a smart pointer. I think it is just easy to use in concurrent environment.

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.