Click here to Skip to main content
15,861,172 members
Articles / Desktop Programming / Win32

tlock<>: Any C++ Object read/write Thread-safe Provider

Rate me:
Please Sign up or sign in to vote.
5.00/5 (34 votes)
13 Jul 2019CPOL2 min read 44K   68   8
An easy locking class for shared/exclusive mutex functions with upgrade/downgrade

Introduction

Sometimes, you need an object to be accessible from many threads, either read only (so all threads can access), or write (so only one thread can access). This is called shared or exclusive access to an object.

The implementation is based on:

Now with upgrade and downgrade mechanism. The github repo includes rw.hpp (all you need) and an example GUI project that paints a box using multiple threads.

Using the Code

You need a proxy class, so when an object's method is called, the lock/unlock mechanism will be compiled automatically:

C++
class proxy 
    {
    private:
        T *const p;
        RWMUTEX* m;
        HANDLE lm = 0;
        int me;
    public:
        proxy(T * const _p, RWMUTEX* _m, int _me) : p(_p), m(_m), 
              me(_me) { if (me == 2) m->LockWrite(); else lm = m->LockRead(); }
        ~proxy() { if (me == 2) m->ReleaseWrite(); else { m->ReleaseRead(lm); lm = 0;} }
        T* operator -> () { return p; }
        const T* operator -> () const { return p; }
        T* getp() { return p; }
        const T* getpc() const { return p; }
    };

The constructor and destructor of this class do all the work. They lock with RWMutex before the object method is to be called, and they unlock after the method has been called.

The tlock class will then look like this:

C++
template <typename T> class tlock
{
private:
    mutable T t;
    mutable RWMUTEX m;

    class proxy
    {
        T *const p;
        RWMUTEX* m;
        HANDLE lm = 0;
        int me;
    public:
        proxy(T * const _p, RWMUTEX* _m, int _me) : p(_p), m(_m), me(_me) 
        { 
            if (me == 2) 
                m->LockWrite(); 
            else lm = m->LockRead(); 
        }
        ~proxy() 
        {
            if (me == 2) 
                m->ReleaseWrite(); 
            else 
            { 
                m->ReleaseRead(lm); 
                lm = 0; 
            } 
        }
        T* operator -> () { return p; }
        const T* operator -> () const { return p; }
        T* getp() { return p; }
        const T* getpc() const { return p; }
        void upgrade() 
        {
            if (me == 1)
            {
                lm = 0;
                m->Upgrade();
                me = 2;
            }
        }

        void downgrade()
        {
            if (me == 2)
            {
                lm = m->Downgrade();
                me = 1;
            }
        }
    };

public:
    template< typename ...Args>
    tlock(Args ... args) : t(args...) {}
    const proxy r() const
    {
        return proxy(&t, &m, 1);
    }
    proxy w()
    {
        return proxy(&t, &m, 2);
    }

    T& direct()
    {
        return t;
    }

    const T& direct() const
    {
        return t;
    }

    void readlock(std::function<void(const T&)> f) const
    {
        proxy mx(&t, &m, 1);
        f(*mx.getp());
    }
    void writelock(std::function<void(T&)> f)
    {
        proxy mx(&t, &m, 2);
        f(*mx.getp());
    }
    
   void rwlock(std::function<void(const T&,std::function<void(std::function<void(T&)>)>)> f)
    {
        proxy mx(&t, &m, 1);
        auto upfunc = [&](std::function<void(T&)> f2)
        {
            mx.upgrade();
            f2(*mx.getp());
            mx.downgrade();
        };
        f(*mx.getp(), upfunc);
    }       

    proxy operator -> () { return w(); }
    const proxy operator -> () const { return r(); }
};

The r() method is called when you want read-only access to the object. This is the default when operator -> is called on a const object.

The w() method is called when you want write access to the object. This is the default for operator -> if the object is not constant.

The readlock() method is called when you want many operations in a locked read-only object, so it calls your function, passing a reference to the constant, locked object.

The writelock() method is called when you want many operations in a locked read-write object, so it calls your function, passing a reference to the locked object.

The rwlock() method is called when you want mainly read operations, but occasionally you want write upgrades. It passes a reference to the locked object and an upgrade function to upgrade the lock.

Let's see some incorrect usage (without tlock):

C++
vector<int> s;
std::thread t1([&]() { s.push_back(0); });
std::thread t2([&]() { s.push_back(1); });
std::thread t3([&]() { s.push_back(2); });
std::thread t4([&]() { s.push_back(3); });
std::thread t5([&]() { s.push_back(4); });
t1.join();t2.join(); t3.join(); t4.join(); t5.join();

Boom!

Image 1

And now, the correct usage:

C++
tlock<vector<int>> s;
std::thread t1([&]() { s->push_back(0); });
std::thread t2([&]() { s->push_back(1); });
std::thread t3([&]() { s->push_back(2); });
std::thread t4([&]() { s->push_back(3); });
std::thread t5([&]() { s->push_back(4); });
t1.join();t2.join(); t3.join(); t4.join(); t5.join();

Now the writing is thread safe.

Using writelock() would be like that:

C++
s.writelock([&](vector<int>& ss)
    {
    ss.push_back(100);
    ss.push_back(150); 
    ss.erase(ss.begin());
    // Safe operations, s is locked while in this function.
    })

And an example usage of rwlock():

C++
s.rwlock([&](const vector<int>& vv, 
             std::function<void(std::function<void(vector<int>&)>)> upgrfunc) 
{
	// vv read access
	upgrfunc([&](vector<int>& nn) 
	{
		// nn write access
		// function end downgrades
	});
});

tlock2 (C++ 17, shared_mutex)

C++
template <typename T> class tlock2
{
private:
    mutable T t;
    mutable std::shared_mutex m;

    class proxy
    {
        T* const p;
        std::shared_mutex* m;
        int me;
    public:
        proxy(T* const _p, std::shared_mutex* _m, int _me) : p(_p), m(_m), me(_me)
        {
            if (me == 2)
                m->lock();
            else
                m->lock_shared();
        }
        ~proxy()
        {
            if (me == 2)
                m->unlock();
            else
                m->unlock_shared();
        }
        T* operator -> () { return p; }
        const T* operator -> () const { return p; }
        T* getp() { return p; }
        const T* getpc() const { return p; }
    };

public:
    template< typename ...Args>
    tlock2(Args ... args) : t(args...) {}
    const proxy r() const
    {
        return proxy(&t, &m, 1);
    }
    proxy w()
    {
        return proxy(&t, &m, 2);
    }

    std::shared_mutex& mut() { return m; }
    T& direct()
    {
        return t;
    }

    const T& direct() const
    {
        return t;
    }

    void readlock(::std::function<void(const T&)> f) const
    {
        proxy mx(&t, &m, 1);
        f(*mx.getp());
    }
    void writelock(::std::function<void(T&)> f)
    {
        proxy mx(&t, &m, 2);
        f(*mx.getp());
    }

    proxy operator -> () { return w(); }
    const proxy operator -> () const { return r(); }
};

This uses std::shared_mutex. The difference with tlock is that it cannot upgrade a reading mutex to a writing one.

History

  • 13-12-2018: Updated based on RWMUTEX for upgradable/downgradable locks, also direct access and upgrade function
  • 12-12-2017: Updated based on RWMUTEX update
  • 30-08-2017: Added readlock and writelock convenience functions
  • 12-05-2017: First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Greece Greece
I'm working in C++, PHP , Java, Windows, iOS, Android and Web (HTML/Javascript/CSS).

I 've a PhD in Digital Signal Processing and Artificial Intelligence and I specialize in Pro Audio and AI applications.

My home page: https://www.turbo-play.com

Comments and Discussions

 
Questionsome proposal Pin
Sergeant Kolja12-Dec-17 10:20
professionalSergeant Kolja12-Dec-17 10:20 
AnswerRe: some proposal Pin
Rick York12-Dec-17 13:06
mveRick York12-Dec-17 13:06 
AnswerRe: some proposal Pin
Michael Chourdakis14-Dec-17 1:39
mvaMichael Chourdakis14-Dec-17 1:39 
AnswerRe: some proposal Pin
Bob100014-Dec-18 4:42
professionalBob100014-Dec-18 4:42 
Questionquestion Pin
PanRuiSen26-Jul-17 15:21
PanRuiSen26-Jul-17 15:21 
AnswerRe: question Pin
Michael Chourdakis10-Aug-17 3:49
mvaMichael Chourdakis10-Aug-17 3:49 
QuestionDiff Pin
_kb_24-May-17 19:55
_kb_24-May-17 19:55 
AnswerRe: Diff Pin
Michael Chourdakis26-May-17 10:54
mvaMichael Chourdakis26-May-17 10:54 

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.