Search code examples
c++11shared-ptrrefcounting

(Ab)using shared_ptr as a reference counter


Recently i thought of a cunning plan(tm :P)) I have to update settings structure in my program(lets say every 15 seconds). Settings structure is used by multiple functions and every of those functions is called by multiple threads. So I need a reference counter to know when it is safe to free the old settings struct. So is this the correct way to do it? Please don't respond that it is OK if you haven't read the code carefully, when it comes to shared pointers it's easy to make mistakes when doing abuses like this(trust me). EDIT:I forgott to mention important part. I think that this implementation prevents the ref counter dropping to 0, because I initialize it in updateSettings() and it doesn't drop until it is called again(and then myFucntion uses the other of the 2 settings in the memory).

#include<memory>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
struct STNGS
{
    int i;
    vector<double> v;
};
static int CUR_STNG=0;
shared_ptr<STNGS> stngsArray[2];
int myFunction() //called by multiple threads
{
    shared_ptr<STNGS> pStngs=stngsArray[CUR_STNG];
    STNGS& stngs=*pStngs;
    //do some stuff using stngs

}

void updateSettings()
{
    auto newIndex=(CUR_STNG+1)%2;
    stngsArray[newIndex].reset(new STNGS);
    CUR_STNG=newIndex;
}
void initialize()
{
    auto newIndex=CUR_STNG;
    stngsArray[newIndex].reset(new STNGS);
    CUR_STNG=newIndex;
}
int main()
{
    initialize();
    //launch bunch of threads that are calling myFunction
    while(true)
    {
        //call updateSettings every 15 seconds
    }
}

EDIT:using feedback from the comments I updated the code:

#include<memory>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
static const int N_STNG_SP=4;
static int CUR_STNG=0;
struct STNGS
{
    int i;
    vector<double> v;
    STNGS()
    {
        for (int i=0;i<10;++i)
            v.push_back(42);
    }
};
shared_ptr<STNGS> stngs[N_STNG_SP];
int myFunction() //called by multiple threads
{
    shared_ptr<STNGS> pStngs=stngs[CUR_STNG];
    STNGS& stngs=*pStngs;
    //do some stuff using stngs
}

void updateSettings()
{
    auto pStng=new STNGS;
    //fill *pStng
    int newVer=(CUR_STNG+1)%N_STNG_SP;
    stngs[newVer].reset(pStng);
    CUR_STNG=newVer;
}
void initialize()
{
    auto pStng=new STNGS;
    //fill *pStng
    int newVer=(CUR_STNG+1)%N_STNG_SP;
    stngs[newVer].reset(pStng);
    CUR_STNG=newVer;
}
int main()
{
    initialize();
    //launch bunch of threads that are calling myFunction
    while(true)
    {
        //call updateSettings every 15 seconds
        updateSettings();
    }
}

Solution

  • I would not trust this code. I believe it is lacking proper memory barriers on all memory shared by the different threads, except for the two reference counts.

    This looks like a good application for shared_mutex to me.

    Edit:

    20.7.2.2 [util.smartptr.shared]/p4 says:

    For purposes of determining the presence of a data race, member functions shall access and modify only the shared_ptr and weak_ptr objects themselves and not objects they refer to.

    However, instead of using a shared_mutex, another option might be to use the API in 20.7.2.5 shared_ptr atomic access [util.smartptr.shared.atomic]:

    Concurrent access to a shared_ptr object from multiple threads does not introduce a data race if the access is done exclusively via the functions in this section and the instance is passed as their first argument.

    template<class T>
        bool atomic_is_lock_free(const shared_ptr<T>* p);
    template<class T>
        shared_ptr<T> atomic_load(const shared_ptr<T>* p);
    template<class T>
        shared_ptr<T> atomic_load_explicit(const shared_ptr<T>* p, memory_order mo);
    template<class T>
        void atomic_store(shared_ptr<T>* p, shared_ptr<T> r);
    template<class T>
        void atomic_store_explicit(shared_ptr<T>* p, shared_ptr<T> r, memory_order mo);
    template<class T>
        shared_ptr<T> atomic_exchange(shared_ptr<T>* p, shared_ptr<T> r);
    template<class T>
        shared_ptr<T>
        atomic_exchange_explicit(shared_ptr<T>* p, shared_ptr<T> r, memory_order mo);
    template<class T>
        bool
        atomic_compare_exchange_weak(shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w);
    template<class T>
        bool
        atomic_compare_exchange_strong( shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w);
    template<class T>
        bool
        atomic_compare_exchange_weak_explicit(shared_ptr<T>* p, shared_ptr<T>* v,
                                              shared_ptr<T> w, memory_order success,
                                              memory_order failure);
    template<class T>
        bool
        atomic_compare_exchange_strong_explicit(shared_ptr<T>* p, shared_ptr<T>* v,
                                                shared_ptr<T> w, memory_order success,
                                                memory_order failure);
    

    shared_mutex will be easier to get right. But the atomic shared_ptr API may yield a higher performance solution.

    Update:

    Here is untested code for the shared_mutex solution (note shared_mutex is not std, but is 3rd party library):

    struct STNGS
    {
        int i;
        vector<double> v;
        ting::shared_mutex m;
    };
    
    STNGS stngs;
    
    int myFunction() //called by multiple threads
    {
        shared_lock<shared_mutex> _(stngs.m);
        //do some stuff using stngs
        return 0;
    }
    
    void updateSettings()
    {
        unique_lock<shared_mutex> _(stngs.m);
        //fill stngs
    }
    
    void initialize()
    {
        //fill stngs
    }
    

    Here is untested code which uses the atomic load/store functions for shared_ptr:

    struct STNGS
    {
        int i;
        vector<double> v;
    };
    
    shared_ptr<STNGS> pStng;
    
    int myFunction() //called by multiple threads
    {
        shared_ptr<STNGS> stngs = atomic_load(&pStng);
        //do some stuff using *stngs
        return 0;
    }
    
    void updateSettings()
    {
        shared_ptr<STNGS> newStng(new STNGS);
        //fill *newStng
        atomic_store(&pStng, newStng);
    }
    
    void initialize()
    {
        pStng.reset(new STNGS);
        //fill *pStng
    }