views:

1137

answers:

10

Currently ive got some reference counted classes using the following:

class RefCounted
{
public:
    void IncRef()
    {
        ++refCnt;
    }
    void DecRef()
    {
        if(!--refCnt)delete this;
    }
protected:
    RefCounted():refCnt(0){}
private:
    unsigned refCnt;
    //not implemented
    RefCounted(RefCounted&);
    RefCounted& operator = (RefCounted&};
};

I also have a smart pointer class that handles reference counting , all though its not uniformly used (eg in one or two bits of performance critical code, where I minimised the number of IncRef and DecRef calls).

template<class T>class RefCountedPtr
{
public:
    RefCountedPtr(T *p)
    :p(p)
    {
        if(p)p->IncRef();
    }
    ~RefCountedPtr()
    {
        if(p)p->DecRef();
    }
    RefCountedPtr<T>& operator = (T *newP)
    {
        if(newP)newP->IncRef();
        if(p)   p   ->DecRef();
        p = newP;
        return *this;
    }
    RefCountedPtr<T>& operator = (RefCountedPtr<T> &newP)
    {
        if(newP.p)newP.p->IncRef();
        if(p)     p     ->DecRef();
        p = newP.p;
        return *this;
    }
    T& operator *()
    {
        return *p;
    }
    T* operator ->()
    {
        return p;
    }
    //comparison operators etc and some const versions of the above...
private:
    T *p;
};

For the general use of the classes themselves I plan to use a reader/writer locking system, however I dont really want to have to get a writer lock for every single IncRef and DecRef call.

I also just thought of a scenario where the pointer may be invalidated just before the IncRef call, consider:

class Texture : public RefCounted
{
public:
    //...various operations...
private:
    Texture(const std::string &file)
    {
        //...load texture from file...
        TexPool.insert(this);
    }
    virtual ~Texture()
    {
        TexPool.erase(this);
    }
    freind CreateTextureFromFile;
};
Texture *CreateTexture(const std::string &file)
{
    TexPoolIterator i = TexPool.find(file);
    if(i != TexPool.end())return *i;
    else return new Texture(file);
}
ThreadA                                ThreadB
t = CreateTexture("ball.png");
t->IncRef();
...use t...                            t2 = CreateTexture("ball.png");//returns *t
...                                    thread suspended...
t->DecRef();//deletes t                ...
...                                    t2->IncRef();//ERROR

So I guess I need to change the ref counting model entirely, the reason I added a ref after the return in the design was to support things like the following:

MyObj->GetSomething()->GetSomethingElse()->DoSomething();

rather than having to:

SomeObject a = MyObj->GetSomething();
AnotherObject *b = a->GetSomethingElse();
b->DoSomething();
b->DecRef();
a->DecRef();

Is there a clean way for fast reference counting in c++ in a multi threaded environment?

+14  A: 

Make the reference counting atomic and you won't need any lock. In Windows ::InterlockedIncrement and ::InterlockedDecrement can be used. In C++ 0x, you have atomic<>.

Edouard A.
Or failing that place critical sections round the potential modifications.
Goz
InterlockedIncrement is a lot faster than critical sections and is available in the win32 API.
Matt Price
This doesn't fix the race; the last reference could be removed between CreateTexture and AddRef with the API given.
bdonlan
Your API is broken, you should create and increase atomically. You cannot have creation and then increment. Both or nothing.
Edouard A.
So how am I supposed to go about fixing it for the texture case? Even if I move IncRef into the CreateTexture method, theres still the point between finding a cached texture and incrementing the reference count in which another thread could delete it.
Fire Lancer
If somecode, somewhere, references the structure, then the reference count must be greater than 0. If not, reference counting will never work. For example this is wrong:protected: RefCounted():refCnt(0){}it should be protected: RefCounted():refCnt(1){}
Edouard A.
Ok, so whats the solution, I dont want TexPool to hold references because textures would never be destroyed when unused.
Fire Lancer
You can make a call in texpoool to release a reference to a specific texture when you acquired it, it's not very beautiful but it will solve your race condition.texture_ptr tex = pool->create();pool->unbind(tex);You can wrap all of this into a template function to make it even safer.
Edouard A.
Im failing to see how that allows one texture to be used multiple times (since textures are immutable) but prevents the pool storing a texture thats not used anywhere?
Fire Lancer
If I underdand this right Edouard A. suggests pool holds its own reference to the texture when returning it from pool->create thus after first operator refCount is 2 for newly created texture. Unless someone else also requested this texture pool->unbind makes it 1 and when tex goes out of scope refCount becomes zero and texture gets destroyed.
Oleg Zhylin
Oleg, that's exactly it. The unbind makes it easier to understand (or not), you can also make an explicit DecRef() call.
Edouard A.
-1 This is REALLY bad, although the number is incremented and decremented "safely", in the time it took to get the pointer, dereference, and then change the value (thats at LEAST 3 instructions) anything could have happened to the object. It may have been deleted and some other object now uses that memory, and you just start changing values in it. Worse still, you're now accessing one class, but treating it as a completely new one. Interlocked functions are really tricky, especially when you start dealing with things like that ABA problem.
Grant Peters
@grant: you're right on one part, interlocked functions are tricky. For the rest I'm not sure I understand your comment. The object is deleted when the reference count reaches 0, no exception. There is no such thing as a transition from 0 to 1.
Edouard A.
+1  A: 

osg, OpenSceneGraph has such a structure.

you derive your classes from osg::Referenced and you dont care about destructor even in multithread.

you just create classes as :

osg::ref_ptr<MyClass> m = new MyClass();

instead of:

MyClass* m = new MyClass();
ufukgun
+10  A: 

Unless you know it's a specific bottleneck I'd just use boost::shared_ptr

It is very fast however there is a bit of extra overhead in the extra control block being allocated. On the other hand it has many benefits:

  • It is portable
  • It is correct
  • You don't have to waste your mental cycles on it leaving you time to actually get stuff done
  • It is fast
  • It is industry standard and other programmers will immediately understand it.
  • It forces you to use boost which if you aren't you should be

Also note, you probably won't want a reader\writer lock for a ref counted object. The contention is minimal and the extra overhead will completely overwhelm any benefits you would have. The shared pointer is implemented with a chip level atomic int operation, this is significantly better than a normal mutex which is significantly faster than a reader\writer lock.

Matt Price
boost::shared_ptr also has got a lock free implementation which makes it a good choice for multithreaded applications, unless it's built with BOOST_SP_NO_ATOMIC_ACCESS
Edouard A.
I explicitly said i didnt want a lock for reference counting because thats lots of locking an unlocking even when just passing data about, I was talking about objects which will have their contents manipulated across several threads :)
Fire Lancer
How does boost::shared_ptr fix the race problem? Its still something that happens after the object is created, and I do not want the pool to maintain a reference since I need to clear up texture not currently in use anywhere, so there is still a point between a cached texture being found and a new reference being created, in which it could be destroyed?
Fire Lancer
The texture race problem is a separate issue. You will need to have a separate lock to protect the map access. You should also note the `std` containers don't have any threading guarantees and will cause you problems with the above code. For example if, while you're searching the tree, you do an deletion or insertion, it can cause undefined behavior.
Matt Price
+1  A: 

boost::shared_ptr and Poco::SharedPtr both wrap this idiom in a freestanding smart pointer.

If you want intrusive reference counting, as you've demonstrated above, Poco's AutoPtr is a good, working implementation.

EDIT: I would have added links, but I was too low on reputation. Google for any of the class names, and you should find your way.

Kim Gräsman
A: 

Your main problem is that you don't acquire a reference before CreateTexture returns. If you're open-coding like this, the easiest way to handle it is to have a lock around TexPool which is also taken when releasing references before the delete, like so:

// PSEUDOCODE WARNING: --refcnt MUST be replaced by an atomic decrement-and-test
// Likewise, AddRef() MUST use an atomic increment.
void DecRef() {
    if (!--refcnt) {
        lock();
        if (!refcnt)
            delete this;
        unlock();
    }
}

and:

Texture *CreateTexture(const std::string &file)
{
    lock();

    TexPoolIterator i = TexPool.find(file);
    if(i != TexPool.end()) {
        *i->AddRef();
        unlock();
        return *i;
    }
    unlock();
    return new Texture(file);
}

That said, as others have mentioned, boost::shared_ptr (aka std::tr1::shared_ptr) implements this all in a lockless, safe way, and also has support for weak pointers, which will help with your texture cache.

bdonlan
+3  A: 

If you don't want to use boost or C++0X, but you still want lockless refcounting, you can do so by including the correct platform-specific atomic-increment/atomic-decrement assembly routines in your code. As an example, here's the AtomicCounter class that I use for my reference counting; it works under most common OS's:

https://public.msli.com/lcs/muscle/html/AtomicCounter_8h_source.html

Yes, it's a nasty mess of #ifdefs. But it does work.

Jeremy Friesner
+1  A: 

Your cache needs to use a boost::weak_ptr or a similar construct.

peterchen
A: 

I think you do need critical sections for this particular design. One place where it is required is CreateTexture, because otherwise you are running into a risk of having more than one identical texture object in the system. And, in general, if several threads can create and destroy the same texture it makes it "mutable shared state".

Oleg Zhylin
+2  A: 

Did you want thread-safe or atomically thread-safe? boot::shared_ptr is merely thread-safe. You still need to "own" a shared_ptr in order to copy it safely.

There's some experimental stuff I did on atomically thread-safe reference counting here at http://atomic-ptr-plus.sourceforge.net/ which can give you an idea of what's involved.

This is an excellent example. See also http://www.drdobbs.com/cpp/184401888;jsessionid=S3LQ44DGLYBR1QE1GHPSKHWATMY32JVN?_requestid=4096 for some abstract overview whats going on.
mtasic
A: 

Take a look at this pdf: http://www.research.ibm.com/people/d/dfb/papers/Bacon01Concurrent.pdf

This describes a reference counting system which does not need any locking. (Well you need to "pause" threads one at a time that can count as locking.) It also collects garbage cycles. The disadvantage is that it is a lot more complex. There are also some important things left as an exercise for the reader. Like what happens when a new thread is created or an old one deleted or how to deal with inherently acyclic objects. (If you decide to do this please let me know how you sloved these.)

stribika