Harris M Snyder
Harris M Snyder

Reputation: 115

Is this inter-thread object sharing strategy sound?

I'm trying to come up with a fast way of solving the following problem:

I have a thread which produces data, and several threads which consume it. I don't need to queue produced data, because data is produced much more slowly than it is consumed (and even if this failed to be the case occasionally, it wouldn't be a problem if a data point were skipped occasionally). So, basically, I have an object that encapsulates the "most recent state", which only the producer thread is allowed to update.

My strategy is as follows (please let me know if I'm completely off my rocker):

I've created three classes for this example: Thing (the actual state object), SharedObject<Thing> (an object that can be local to each thread, and gives that thread access to the underlying Thing), and SharedObjectManager<Thing>, which wraps up a shared_ptr along with a mutex.

The instance of the SharedObjectManager (SOM) is a global variable. When the producer starts, it instantiates a Thing, and tells the global SOM about it. It then makes a copy, and does all of it's updating work on that copy. When it is ready to commit it's changes to the Thing, it passes the new Thing to the global SOM, which locks it's mutex, updates the shared pointer it keeps, and then releases the lock.

Meanwhile, the consumer threads all intsantiate SharedObject<Thing>. these objects each keep a pointer to the global SOM, as well as a cached copy of the shared_ptr kept by the SOM... It keeps this cached until update() is explicitly called.

I believe this is getting hard to follow, so here's some code:

#include <mutex>
#include <iostream>
#include <memory>

class Thing
{
    private:
        int _some_member = 10;
    public:
        int some_member() const { return _some_member; }
        void some_member(int val) {_some_member = val; }
};

// one global instance
template<typename T>
class SharedObjectManager
{
    private:
        std::shared_ptr<T> objPtr;
        std::mutex objLock;

    public:
        std::shared_ptr<T> get_sptr()
        {
            std::lock_guard<std::mutex> lck(objLock);
            return objPtr;
        }

        void commit_new_object(std::shared_ptr<T> new_object)
        {
            std::lock_guard<std::mutex> lck (objLock);
            objPtr = new_object;
        }
};


// one instance per consumer thread.
template<typename T>
class SharedObject
{
    private:
        SharedObjectManager<T> * som;
        std::shared_ptr<T> cache;

    public:
        SharedObject(SharedObjectManager<T> * backend) : som(backend)
        {update();}

        void update()
        {
            cache = som->get_sptr();
        }

        T & operator *()
        {
            return *cache;
        }

        T * operator->()
        {
            return cache.get();
        }


};



// no actual threads in this test, just a quick sanity check.

SharedObjectManager<Thing> glbSOM;

int main(void)
{
    glbSOM.commit_new_object(std::make_shared<Thing>());

    SharedObject<Thing> myobj(&glbSOM);

    std::cout<<myobj->some_member()<<std::endl;
    // prints "10".
}

The idea for use by the producer thread is:

// initialization - on startup
auto firstStateObj = std::make_shared<Thing>();
glbSOM.commit_new_object(firstStateObj);

// main loop
while (1)
{
    // invoke copy constructor to copy the current live Thing object
    auto nextState = std::make_shared<Thing>(*(glbSOM.get_sptr()));

    // do stuff to nextState, gradually filling out it's new value 
    //    based on incoming data from other sources, etc.
    ...

    // commit the changes to the shared memory location
    glbSOM.commit_new_object(nextState);
}

The use by consumers would be:

SharedObject<Thing> thing(&glbSOM);
while(1)
{
    // think about the data contained in thing, and act accordingly...
    doStuffWith(thing->some_member());        

    // re-cache the thing
    thing.update();
}

Thanks!

Upvotes: 1

Views: 86

Answers (1)

SergeyA
SergeyA

Reputation: 62603

That is way overengineered. Instead, I'd suggest to do following:

  • Create a pointer to Thing* theThing together with protection mutex. Either a global one, or shared by some other means. Initialize it to nullptr.
  • In your producer: use two local objects of Thing type - Thing thingOne and Thing thingTwo (remember, thingOne is no better than thingTwo, but one is called thingOne for a reason, but this is a thing thing. Watch out for cats.). Start with populating thingOne. When done, lock the mutex, copy thingOne address to theThing, unlock the mutex. Start populating thingTwo. When done, see above. Repeat untill killed.
  • In every listener: (make sure the pointer is not nullptr). Lock the mutex. Make a copy of the object pointed two by the theThing. Unlock the mutex. Work with your copy. Burn after reading. Repeat untill killed.

Upvotes: 1

Related Questions