c++multithreadingtemplatesboostconcurrency

Making a C++ class a Monitor (in the concurrent sense)


I want to ensure that only one thread at a time can run a method of my C++ class. In other words, make the class behave like a Monitor.

Is there a pattern, templatized way to do this, or some Boost class I can use? Because my only idea so far is adding a Critical Section member, and acquire it at the beginning of each method and release it at the end (using RAII, of course). But that seems very redundant, and I can't reuse it for some other class.


Solution

  • You can achieve this with some judicious use of operator-> and modern c++ which gives for much cleaner syntax than the previously accepted answer:

    template<class T>
    class monitor
    {
    public:
        template<typename ...Args>
        monitor(Args&&... args) : m_cl(std::forward<Args>(args)...){}
    
        struct monitor_helper
        {
            monitor_helper(monitor* mon) : m_mon(mon), m_ul(mon->m_lock) {}
            T* operator->() { return &m_mon->m_cl;}
            monitor* m_mon;
            std::unique_lock<std::mutex> m_ul;
            T& operator*(){return m_mon->m_cl;} // For accessing using a manually locked object
    
        };
    
        monitor_helper operator->() { return monitor_helper(this); }
        monitor_helper ManuallyLock() { return monitor_helper(this); }
        T& GetThreadUnsafeAccess() { return m_cl; }
    
    private:
        T           m_cl;
        std::mutex  m_lock;
    };
    

    The idea is that you use the arrow operator to access the underlying object, but that returns a helper object which locks and then unlocks the mutex around your function call. Then through the magic of the language repeatedly applying operator-> you get a reference to the underlying object.

    Usage:

    monitor<std::vector<int>> threadSafeVector {5};
    
    threadSafeVector->push_back(0);
    threadSafeVector->push_back(1);
    threadSafeVector->push_back(2);
    
    // Create a bunch of threads that hammer the vector
    std::vector<std::thread> threads;
    for(int i=0; i<16; ++i)
    {
        threads.push_back(std::thread([&]()
        {
            for(int i=0; i<1024; ++i)
            {
                threadSafeVector->push_back(i);
            }
        }));
    }
    
    // You can explicitely take a lock then call multiple functions
    // without the overhead of a relock each time. The 'lock handle'
    // destructor will unlock the lock correctly. This is necessary
    // if you want a chain of logically connected operations 
    {
        auto lockedHandle = threadSafeVector.ManuallyLock();
        if(!lockedHandle->empty())
        {
            lockedHandle->pop_back();
            lockedHandle->push_back(-3);
        }
    
        // With a locked handle you can get the raw object via de-reference. As long as the lockedHandle stays in scope using the raw object is ok.
        for (auto val : *lockedHandle)
        {
            std::cout << "Val = " << val << std::endl;
        }
    }
    
    for(auto& t : threads)
    {
        t.join();
    }
    
    // And finally access the underlying object in a raw fashion without a lock
    // Use with Caution!
    
    std::vector<int>& rawVector = threadSafeVector.GetThreadUnsafeAccess();
    rawVector.push_back(555);
    
    // Should be 16393 (5+3+16*1024+1)
    std::cout << threadSafeVector->size() << std::endl;