Skip to main content
GameDev.net gamedev.net
🔒 Locked

RefCount and Manager

Started by Alundra May 2, 2014 at 3:32 AM 21 replies 6.4k views
Original Post
Alundra
Alundra

Hi,

When working on managers you generally end to refcount.

Is it really needed to have a refcount ? Refcount needs to be thread safe, a lock is needed.

Is it better to have a base class IRefCount and inherit from it or have that in manager ?

Is it better to have Remove or Release in manager or just create and let the user release ?

Thanks

nfactorial
nfactorial

You don't need a lock for a reference count, you can just use atomic operations (such as InterlockedIncrement etc. on windows, other platforms have similar).

I feel a 'manager' implies you don't need a ref-count, as the manager would manage the lifetime.

However, whether a ref-count is good or necessary for you is entirely down to what you're doing and the behaviour you desire. There's not really any complete answer without more detail.

n!

Alundra
Alundra

For GameState, the GameStateManager doesn't needs ref count of course, same for ActionManager who handle inputs.

RefCount should be used for Texture, Mesh, Material, Sound and Animation. The question is for these type of objects.

For animation, the ref count could be avoided I think if the AnimationSystem handle an AnimationSet.

L. Spiro
L. Spiro

You don't need a lock for a reference count, you can just use atomic operations (such as InterlockedIncrement etc. on windows, other platforms have similar).

Except that upon decreasing a count you may also need to delete/release the resource, and during that whole process you need to maintain a lock.
In an atomic operation, you can indeed correctly enter an “if” when it reaches 0 and begin a segment of code to delete the OpenGL ID or Direct3D handle/pointer, except that suddenly another thread is free to raise that reference count back to 1 and then use that deleted resource.


L. Spiro
I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid
haegarr
haegarr

Just FYI: Also for resources it depends on the handling policy whether reference counting is necessary at all. For example, you can use a policy based on potential use: Load and unload resources when reaching "natural" borders like levels or in-game regions. Even if you want to hold resources that are shared by consecutive levels or regions, you can do so without reference counting but using a kind of caching (although this can be understood as a dual valued reference counting). And if you do reference counting, e.g. because of a resource may belong to several simultaneously loaded tiles of an virtually unlimited streamed-in terrain, you still not need synchronization if all reference counting is done in the loader thread. Real reference counting is needed only if the moment of unloading may happen at any time.

nfactorial
nfactorial
[Except that upon decreasing a count you may also need to delete/release the resource, and during that whole process you need to maintain a lock.

No, you don't need to maintain a lock at all. If it has 0 references, then there is nothing referencing it, so nothing else could actually gain access to it for the lock to protect against.



In an atomic operation, you can indeed correctly enter an “if” when it reaches 0 and begin a segment of code to delete the OpenGL ID or Direct3D handle/pointer, except that suddenly another thread is free to raise that reference count back to 1 and then use that deleted resource.

This logic doesn't work sorry, another thread cannot suddenly raise the reference back to 1. Because there is nothing else referencing it for that to be possible.

n!

nfactorial
nfactorial



RefCount should be used for Texture, Mesh, Material, Sound and Animation. The question is for these type of objects.

There is no *should* here, it is an option of-course. But it is entirely dependent on how you structure the management of these resources.

I'm neither for or against reference counting, I think it's a fine solution for many things but it isn't the only solution.

For example, lets say we have some resource manager objects that manages a list of resources and destroys them all in its destructor.

We can have:

Scene

TextureManager

ModelManager

When a model asks for a texture it goes through the scenes texture manager. When the scene is destroyed. All models are destroyed and all textures, no reference counting necessary.

Simply following the same method we can extend it further with a hierarchy:

Application

TextureManager

ModelManager

Scene

TextureManager

ModelManager

The lifetime is simply controlled by the containers lifetime, at it is not possible for something higher up the hierarchy to access something lower down. It is a simple alternative, it is not a complete solution. More a quick overview to show why it isn't *necessary* for reference counting. But please don't take this as a "you shouldn't" use reference counting. It's a decision you can make as a developer.

n!

samoth
samoth

It should be clear what a refcount is wanted for -- the manager or the objects it manages.

For the managed objects, it usually makes sense, simply because it's the easiest and least failsafe way of doing things (which neither risks leaking, nor destroys objects while in-use). Holding a shared_ptr and handing out objects by weak_ptr is one approach of doing this in a very easy, safe, and straightforward way, without rewriting a lot of crud. This uses what the standard (and countless libraries) readily provides. The question where to release a resource doesn't even arise, you just toss the pointer.

Of course, refcounting is by no means necessary, with some discipline it is very well possible to do without.

As for the manager, when people say "manager" and "refcount" in one sentence they most commonly mean "uh you know, singleton, kind of, and threadsafe and initializing thread-safely too, and...." when in fact all most of the time all that's needed and desirable is one instance of a class that is created as the first thing in main. That's not only the easiest thing to do, it also solves all initialization and lifetime issues, and half of the threading issues.

Alundra
Alundra
Of course, refcounting is by no means necessary, with some discipline it is very well possible to do without.

Yeah, of course it can be without, if you know you load all object needed then clear managers when changing scene.

But that only works when you are absolutely sure of resources used, for an editor you load/release a lot.

RefCount is a solution who works for all ways, editor or in game, this is why I think it should be used ?

[EDIT] Never mind, talking rubbish really...

https://www.ruby-lang.org ? mellow.png

samoth
samoth
for an editor you load/release a lot

You do in the game too. All the time.

If you don't, that is if you only have a handful of resources, then you don't really need a manager. The point of a manager (say, texture manager) is that something (e.g. texture memory) is limited, so you can only hold so and so many at a time. It manages what you have versus what you need at any one time, those are not necessarily the same. You can't hold 20 gigabytes of music in RAM just like that, you need to prioritize what you need now and what you can throw away. Much like, or almost identical to, a cache.

Therefore, the manager not only manages creation, but also the lifetime of objects. This must happen in a somewhat safe manner, with safe being more important than fast, really.

It sucks big time if you delete a texture or a buffer object while you are still drawing from it (well, that isn't really possible, but similar stuff is possible). In the best case you see some black triangles, in the worst case your program crashes (or the user component of the display driver does).

Reference counting is very convenient because it works invisibly but reliably for that exact purpose. The "manager" may schedule objects for deletion, and they're kicked out of existence immediately as soon as the last user has stopped holding a reference, but not any earlier.

Ravyne
Ravyne

Frankly, assuming C++ with a modern environment, you should probably just use std::shared_ptr (or possibly std::unique_ptr, depending on the structure of your manager), adding a custom deleter if you need. There's no need to reinvent the ref-counting wheel. Believe it or not, getting ref-counting to work exactly right is not easy -- not when you consider multi-threading, circular dependencies (std::weak_ptr breaks these), and possibly exceptions.

throw table_exception("(? ???)? ? ???");
Alundra
Alundra
you should probably just use std::shared_ptr (or possibly std::unique_ptr, depending on the structure of your manager)

So, for you ref counting should be in manager using smart pointer.

What you suggest is c++11, is it safe to use it nowadays ?

L. Spiro
L. Spiro


Except that upon decreasing a count you may also need to delete/release the resource, and during that whole process you need to maintain a lock.


No, you don't need to maintain a lock at all. If it has 0 references, then there is nothing referencing it, so nothing else could actually gain access to it for the lock to protect against.



In an atomic operation, you can indeed correctly enter an “if” when it reaches 0 and begin a segment of code to delete the OpenGL ID or Direct3D handle/pointer, except that suddenly another thread is free to raise that reference count back to 1 and then use that deleted resource.

This logic doesn't work sorry, another thread cannot suddenly raise the reference back to 1. Because there is nothing else referencing it for that to be possible.


That is a good point.


L. Spiro
I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid
Ravyne
Ravyne

All modern c++ environments have it, its even been in Visual Studio since, IIRC, VS2010 through the tr1 namespace. Otherwise, Boost (which the the standard smart pointers are based on) has you covered with support stretching back even further.

Ref counting is usually a consequence of such managers, but whether the manager itself actually holds an interest in the managed object is up to you. If your philosophy is that the manager always holds an interest in the object, then it should hold a shared_ptr itself, and hand out shared_ptrs to clients, this is arguably the most straight-forward system but you have to periodically sweep teh manager for resources that only it owns when you want to eject them; if your philosophy is that the manager exists *only* to give out handles to resources it loads (without redundantly loading resources)--and if you are happy with releasing resources aggressively-- then it should hold a weak_ptr itself and hand out shared_ptrs to clients. This effectively ejects a resource as soon as it goes unused, but you still need to sweep the manger occasionally for weak_ptrs that don't reference anything.

If you wanted resources to be released lazily (in other words, maybe you don't want to free the resource immediately because you want to do it at a convenient time, or you want to keep everything loaded in memory until memory becomes a problem) then you need to define what your garbage collection policy is, and some combination of smart pointers and sweeping will get you there. This is the most involved approach, and probably more than what most small project need.

throw table_exception("(? ???)? ? ???");
samoth
samoth



If your philosophy is that the manager always holds an interest in the object, then it should hold a shared_ptr itself, and hand out shared_ptrs to clients, this is arguably the most straight-forward system but you have to periodically sweep teh manager for resources that only it owns when you want to eject them
The nice thing about that design is that it isn't actually necessary to sweep periodically. You can hold the shared pointers in your manager (= cache) indefinitely as long as your resource quota allows. Only start searching for a candidate to delete when you get close to the limit (that is, when you are close to running out of your memory limit or such).

And, you can "delete" objects at any time simply by tossing the shared pointer, without risking that things go wrong in the consumers, which is also very nice. Anyone using/consuming a resource will lock their weak pointer temporally, obtaining a valid shared pointer or obtaining a null pointer (either one or the other). So it is always well-defined and well-known whether a resource is valid for a given operation.

The only downside is that in theory it's possible for an object to become "undeletable" if there is always at least one user, since then tossing the manager's reference doesn't get the refcount down to zero. But then again, if consumers are written properly, this won't happen.

The opposite approach (storing a weak pointer in the manager) is admittedly a bit queer, although I don't doubt it would work. The weak pointer basically only exists so it can be locked and a shared pointer be handed out -- if the locking succeeds (which isn't guaranteed). So the manager's get_resource function (or what it's called) would need to communicate failure as well.

0xFFFF0000
0xFFFF0000

Except that upon decreasing a count you may also need to delete/release the resource, and during that whole process you need to maintain a lock.


No, you don't need to maintain a lock at all. If it has 0 references, then there is nothing referencing it, so nothing else could actually gain access to it for the lock to protect against.



In an atomic operation, you can indeed correctly enter an “if” when it reaches 0 and begin a segment of code to delete the OpenGL ID or Direct3D handle/pointer, except that suddenly another thread is free to raise that reference count back to 1 and then use that deleted resource.

This logic doesn't work sorry, another thread cannot suddenly raise the reference back to 1. Because there is nothing else referencing it for that to be possible.
That is a good point.


L. Spiro

How about a situation like this:


1  struct RefCounter
2  {
3     void AddRef()
4     {
5       AtomicIncrement(RefCount);
6     }
7     void ReleaseRef()
8     {
9       AtomicDecrement(RefCount);
10       if(RefCount == 0)
11       {
12          DestroyMe();
13       }
14     }
15     int RefCount;
16  }


With two threads running, one calls AddRef and the other ReleaseRef. Execution could proceed like below.

Thread 1 Thread 2

line 4 (trying to add a ref) line 9 (trying to destroy a ref)

line 10, 11, 12 (destroys object)

line 5 (crashes)

Seems like you need to lock to do something like this.

tanzanite7
tanzanite7

Seems like you need to lock to do something like this.

Whatever is calling the AddRef must be owning a reference - same goes for ReleaseRef. Therefore refcount >= 2 and thread2 cannot trigger object deletion (except programmer errors a'la: violation of c++ concurrency memory model).

No problem.
0xFFFF0000
0xFFFF0000

Seems like you need to lock to do something like this.

Whatever is calling the AddRef must be owning a reference - same goes for ReleaseRef. Therefore refcount >= 2 and thread2 cannot trigger object deletion (except programmer errors a'la: violation of c++ concurrency memory model).

No problem.

It does not own the reference until after AddReference has been called though, as this is what is incrementing the reference count.

L. Spiro
L. Spiro

How about a situation like this:

  struct RefCounter
  {
     void AddRef()
     {
       AtomicIncrement(RefCount);
     }
     void ReleaseRef()
     {
       AtomicDecrement(RefCount);
       if(RefCount == 0)
       {
          DestroyMe();
       }
     }
     int RefCount;
  }



Rather than decrementing and then rechecking RefCount (you are not longer atomically reading it this way), you should read the return of AtomicDecrement() itself (InterlockedDecrement() returns the new decremented value).

     void ReleaseRef()
     {
       if(AtomicDecrement(RefCount) == 0)
       {
          DestroyMe();
       }
     }

It does not own the reference until after AddReference has been called though, as this is what is incrementing the reference count.

If the object exists on 2 threads at once then the reference count must be 2. From there you can’t get to 0 on either thread (hacking aside) without the other thread releasing its reference, after which it can’t get it back. So if the second thread is then releasing its reference, the first thread can’t suddenly increase its reference.

Even if it did, critical sections/mutexes wouldn’t actually prevent a crash/undefined behavior.
Thread 0 would wait for thread 1 to fully decrease and delete the object, then without a second if() check it will continue inside the critical section to increase the reference count and later cause an exception.
If you do use an extra if() inside all the methods on a reference counter you are looking at serious performance penalties.



I was pretty tired in my original post, since I have always known that atomics are used in reference counters, plus my mention of critical sections/mutexes wouldn’t save the day anyway unless you are willing to suffer a huge penalty in performance (which is why everything is done with the slow but less-so atomic method).


L. Spiro
I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.