Jump to content

  • Log In with Google      Sign In   
  • Create Account

FREE SOFTWARE GIVEAWAY

We have 4 x Pro Licences (valued at $59 each) for 2d modular animation software Spriter to give away in this Thursday's GDNet Direct email newsletter.


Read more in this forum topic or make sure you're signed up (from the right-hand sidebar on the homepage) and read Thursday's newsletter to get in the running!


Smart pointers question


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
36 replies to this topic

#21 KaiserJohan   Members   -  Reputation: 1241

Like
0Likes
Like

Posted 04 April 2012 - 08:34 AM

Hey, got a few more questions

1. I realised when I want to update all my Game Objects it seems there is an issue with weak_ptr.

For example, I have a vector of game objects that have registered that they want to receive Update() on each engine update (what is the best way to check that btw, besides a method call to the ObjectFactory that it wants to receive Updates()?)

Now on each engine update, when I access the GameObject in the weak_ptr I have to first go lock() and then get(). Lock creates and returns a shared_ptr, which is quite expensive is it not, to do on every update for all objects? Yet I don't want a shared_ptr in my ObjectFactory because then it would never go out of scope untill Engine destructs (as mentioned earlier in thread)

2. Multithreading; there will probably be trouble if one thread wants to DestroyObject() while an engine thread is Updating() that same object. Having to lock my vector every Update()/Create()/Destroy() seems really inefficient. What is the best way to solve this?
I suppose ObjectFactory could have an internal thread with a message queue, and that thread alone has access rights to explicitly create/destroy/access GameObjects... but isn't that alot of overhead? and dosn't the message queue itself need to be locked? Must also each Update() call from Engine be a message? Would be quite alot of messages.

Also how would it be possible? I mean I realistically can't block untill I get my object from the factory right?

Likewise I plan on using a Component-based model for my GameObjects, so I will have AudioManager, Renderer, etc likewise do their calculations (rather than each gameobject do itm from what i've read on data-driven design). Do they also need to have worker threads like that?

Sponsor:

#22 Trienco   Crossbones+   -  Reputation: 2224

Like
0Likes
Like

Posted 04 April 2012 - 12:33 PM

Basically you have listed two good reasons why you should probably not store weak_ptr in your factory (which I'd call manager instead, since from a factory I'd expect nothing but creating objects). Based on your description this IS pretty much the owner of the objects, so if anything is storing a shared_ptr to them, it should be here. A somewhat weird way to still get what you want could be to check the number of references and remove the shared_ptr if this count is one (ie. this is the only one left). It probably beats a ton of lock() calls.

On the bright side, the object can not be destroyed in a different thread while you're running update, because you created a shared_ptr before calling it and hopefully don't let it go out of scope until update returns. You DO have to check the shared_ptr to be valid before calling update though (unless update itself is checking the pointer for null).
f@dzhttp://festini.device-zero.de

#23 KaiserJohan   Members   -  Reputation: 1241

Like
0Likes
Like

Posted 04 April 2012 - 02:03 PM

According to boost documentation checking the ref count seems to be expensive though (http://www.boost.org...r.htm#use_count)

How does game engines typically handle game object creation? This cant be the first time someone bumps into this issue

#24 Cornstalks   Crossbones+   -  Reputation: 6991

Like
0Likes
Like

Posted 04 April 2012 - 08:12 PM

20+ years of programming experience.

20+ years of sh*t coding still makes you a sh*t coder.
[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#25 SimonForsman   Crossbones+   -  Reputation: 6318

Like
0Likes
Like

Posted 04 April 2012 - 09:50 PM


20+ years of programming experience.

20+ years of sh*t coding still makes you a sh*t coder.


He is a troll, check his profile page, he has andy harglesis in his contacts (and andy has him) which says it all really. (its probably the same person even)
I don't suffer from insanity, I'm enjoying every minute of it.
The voices in my head may not be real, but they have some good ideas!

#26 Hodgman   Moderators   -  Reputation: 31920

Like
0Likes
Like

Posted 04 April 2012 - 09:50 PM

Now on each engine update, when I access the GameObject in the weak_ptr I have to first go lock() and then get(). Lock creates and returns a shared_ptr, which is quite expensive is it not, to do on every update for all objects? Yet I don't want a shared_ptr in my ObjectFactory because then it would never go out of scope untill Engine destructs (as mentioned earlier in thread)

Yes, that's a complete waste of processing time, only used out of programmer laziness.
What's worse, is that in order to be "thread safe", when shared_ptr increments/decrements the object's reference counter, it has to do so in a mutually exclusive way... so you're adding (very lightweight) threading sync-points in every location where you copy shared_ptr's...

2. Multithreading; there will probably be trouble if one thread wants to DestroyObject() while an engine thread is Updating() that same object. Having to lock my vector every Update()/Create()/Destroy() seems really inefficient. What is the best way to solve this?

Don't use shared_ptr's - they're not designed for efficient multi-threaded use.
However... not using shared_ptr's is more advanced, and writing efficient multi-threaded code is also an advanced subject, so I would recommend that most people write single-threaded code using shared_ptr's Posted Image

If we apply a bit of data-oriented design, you can ref-count objects in a wait-free manner. All you need is for each object to have a ref-counter per frame, and to only increment/decrement your counters during your update, and evaluate whether any hit 0 outside of your update (with a single sync point between the two phases).
int refCounters[numObjects][numThreads] = {};
int possibleGarbage[maxDeletesPerFrame][numThreads] = {};
int possibleGarbageCount[numThreads] = {}
void IncRefCount( int object )
{
  assert( IsInUpdatePhase() );
  ++refCounters[object][g_tls_thread_idx];
}

void DecRefCount( int object )
{
  assert( IsInUpdatePhase() );
  --refCounters[object][g_tls_thread_idx];
  possibleGarbage[ possibleGarbageCount[g_tls_thread_idx]++ ][g_tls_thread_idx] = object;
}
int GetRefCount( int object )
{
  assert( !IsInUpdatePhase() );
  int count = 0;
  for( int i=0; i<g_thread_count; ++i )
	count += refCounters[object][i];
  assert( count >= 0 );
  return count;
}
void CheckForGarbage()
{
  assert( !IsInUpdatePhase() );
  int buffer[maxDeletesPerFrame];
  int numUniqueItems = 0;
  for( int i=0; i<g_thread_count; ++i )
	copy_unique_items( buffer, &numUniqueItems, possibleGarbage[i], possibleGarbageCount[i] );
  for( int i=0, i!=numUniqueItems; ++i )
  {
	int refCount = GetRefCount( buffer[i] );
	if( refCount == 0 )
	  DeleteObject( buffer[i] );//remove from listeners arrays
  }
}


#27 Trienco   Crossbones+   -  Reputation: 2224

Like
0Likes
Like

Posted 04 April 2012 - 11:58 PM

According to boost documentation checking the ref count seems to be expensive though (http://www.boost.org...r.htm#use_count)


Good point. I didn't consider that to be thread safe there will have to be some locking going on when reading the counter. So I guess if you want to use shared_ptr, you should try to minimize the creation and destruction (and especially copying) of shared_ptr.

Putting together something less generic with atomic<int> as a ref counter should probably be easy. Or as the above suggestion, coming up with a scheme that avoids extensive syncing in the first place.
f@dzhttp://festini.device-zero.de

#28 Hodgman   Moderators   -  Reputation: 31920

Like
0Likes
Like

Posted 05 April 2012 - 12:24 AM

Putting together something less generic with atomic as a ref counter should probably be easy.

Boost's shared_ptr will use atomic ints, which is the problem. They're atomic so that they're "thread safe", but that's such a vague term, and the specific guarantee given in this case is pretty useless (you'll still get race conditions with shared_ptrs if two threads try to read/write one concurrently).
Just like how wrapping all member functions in a mutex is a bad way to enable "thread safety", making a shared variable into an atomic is just as questionable. Especially in this case, where even after adding the atomic, they can't give a decent thread safety guarantee...

Also, what about people writing single-threaded code (or properly scheduled multi-threaded code) -- why should their usage of shared_ptr be 100x slower, just in case you need their useless "thread safety" guarantee? Well written multi-threaded code doesn't need shared variables, so enforcing atomic counters is forcing a bad design choice onto your users...

#29 Trienco   Crossbones+   -  Reputation: 2224

Like
0Likes
Like

Posted 05 April 2012 - 03:29 AM

They're atomic so that they're "thread safe", but that's such a vague term, and the specific guarantee given in this case is pretty useless (you'll still get race conditions with shared_ptrs if two threads try to read/write one concurrently).


Just to be sure we are talking about the same thing. When you say read/write to the pointer, are you talking about modifying the reference count (I was under the assumption that really was thread-safe) or actually reading/writing the data behind the pointer (which the shared_ptr itself doesn't and shouldn't really care about)?

Also, what about people writing single-threaded code (or properly scheduled multi-threaded code) -- why should their usage of shared_ptr be 100x slower, just in case you need their useless "thread safety" guarantee? Well written multi-threaded code doesn't need shared variables, so enforcing atomic counters is forcing a bad design choice onto your users...


Good point. It reminds me of writing my own shared_ptr (because boost was no option at work). It had pretty much two orthogonal policies. Using a standalong reference counter or using the one in the class itself (this was automatically selected based on whether the class was derived from RefCounted) and whether your intended use was single- or multithreaded (to avoid usage of mutexes when it's not needed).

Though I obviously agree that any design that doesn't need synchronization is better than trying to find a better synchronization method.

One option I like for simplicity and flexibility (if you really can't come up with a better plan) is exploiting the -> operator to allow calling any function of the object with a lock. However the times this is useful are rare, it has a good bit of overhead and if not used carefully might easily cause deadlocks. So I guess 99% of the time it is pretty gimmicky just to be able to write myObject->someFunction() or myObject->lockedAccess()->someFunction(). In fact, I might wonder if a design where a function sometimes requires locking and sometimes doesn't might not be kind of flawed and confusing to the user.
f@dzhttp://festini.device-zero.de

#30 Hodgman   Moderators   -  Reputation: 31920

Like
0Likes
Like

Posted 05 April 2012 - 08:06 AM

Just to be sure we are talking about the same thing. When you say read/write to the pointer, are you talking about modifying the reference count (I was under the assumption that really was thread-safe) or actually reading/writing the data behind the pointer (which the shared_ptr itself doesn't and shouldn't really care about)?

Yeah I mean if two threads are reading/writing the shared_ptr object itself (i.e. changing the ref-counter that it points to). If there's only one reference, and then two threads try to simultaneously (1) remove that only reference, and (2) add a new reference, then it's possible for thread (1) to delete the ref-counter *after* thread (2) has obtained a pointer to it, but *before* thread 2 has yet incremented the counter it points to. This is basically a dangling pointer bug caused by race condition.

i.e. Initial state: shared_ptr<T> a = new ...;
Then simultaneously:
Thread 1: a = null;
Thread 2: shared_ptr<T> b = a;
Thread 2:
-- if( a is not null )
--   RefCounter* refCounter = a.refCounter;//get a pointer to the ref-count to increment it
... interrupted ...
Thread 1:
-- newCount = a.refCounter->Decrement();
-- if( 0==newCount )
--   delete a.refCounter;

Thread 2:
--   refCounter->Increment(); //oops, already deleted the ref-counter
The boost page doesn't describe the actual problem, but it does warn you that you can't read+write a shared_ptr from two different threads.

Apparently in C++11, shared_ptr is more "thread safe", allowing you to concurrently read/write without experiencing the above crash... however, that's even worse, because to overcome the above issue, you've got to add a lot of extra complexity to the memory manager that allocates/deletes your ref-counters.... which is unnecessary in a properly designed multi-threaded system... Posted Image

#31 SiCrane   Moderators   -  Reputation: 9673

Like
1Likes
Like

Posted 05 April 2012 - 08:26 AM

Also, what about people writing single-threaded code (or properly scheduled multi-threaded code) -- why should their usage of shared_ptr be 100x slower, just in case you need their useless "thread safety" guarantee? Well written multi-threaded code doesn't need shared variables, so enforcing atomic counters is forcing a bad design choice onto your users...

Fortunately, boost has options for removing thread safety overhead if you're writing single threaded code. Defining BOOST_DISABLE_THREADS will disable thread support for all of boost (commonly done through boost/config/user.hpp) and BOOST_SP_DISABLE_THREADS will disable it for shared_ptr.

#32 SimonForsman   Crossbones+   -  Reputation: 6318

Like
0Likes
Like

Posted 05 April 2012 - 10:07 AM

Apparently in C++11, shared_ptr is more "thread safe", allowing you to concurrently read/write without experiencing the above crash... however, that's even worse, because to overcome the above issue, you've got to add a lot of extra complexity to the memory manager that allocates/deletes your ref-counters.... which is unnecessary in a properly designed multi-threaded system... Posted Image


Isn't it far better to have the language provide safe and easy to use tools out of the box and let the programmer develop their own faster, unsafe versions if needed than to do things the other way around though ?
I don't suffer from insanity, I'm enjoying every minute of it.
The voices in my head may not be real, but they have some good ideas!

#33 Antheus   Members   -  Reputation: 2397

Like
1Likes
Like

Posted 05 April 2012 - 10:22 AM

Isn't it far better to have the language provide safe and easy to use tools out of the box and let the programmer develop their own faster, unsafe versions if needed than to do things the other way around though ?


Use C# then. That is the "safe and easy" language.

It has better memory model which solves such problems systematically without overhead.

Shared_ptrs invalidate the use of C++. All you end up is overly verbose, slow-to-compile, vastly inefficient language that is worse than managed platforms in all things that matter.

Use C++ if you want or need to manage memory and other hardware resources yourself. Otherwise, managed languages win just about every time.


None of this means that shared_ptrs and similar have no place in C++. But they do expose the commonly missing separation between resource management and resource usage. Ideally, being C++, code would look like this:
void mutator(Foo * fooRef);
void mutator(Foo & fooRef);

mutator(my_sp->get());
mutator(*my_sp);
Or something similar. Reason being shared pointers don't cover all important cases, they merely help sharing ownership. Shared pointers should only be used for sharing, which is surprisingly rare.

They will completely fail if used like Java/C#, which share references like they are going out of fashion. Shared_ and other pointers don't make ownership problems go away but merely make associated problems simpler.


Not separating resource management from use however will result in all of the above problems, including potentially exponential time complexity of individual operations.

#34 SimonForsman   Crossbones+   -  Reputation: 6318

Like
0Likes
Like

Posted 05 April 2012 - 11:59 AM

Isn't it far better to have the language provide safe and easy to use tools out of the box and let the programmer develop their own faster, unsafe versions if needed than to do things the other way around though ?


Use C# then. That is the "safe and easy" language.


So you are saying that because C# is safe and easy C++ shouldn't have a threadsafe shared pointers ? (What about a string class and dynamic containers are those bad things to have in a C++ standard library aswell ?), The fact that another language is safe and easy is not a valid argument for why C++ shouldn't provide a safe and easy to use standard library, The low level stuff is still there to be used WHEN they are needed (Which almost never is 100% of your codebase)

Yes, shared pointers shouldn't be used as you use C# or Java references, they should only be used to share ownership of data(Which is why they exist) and having a thread safe reference count when multiple threads can own the same data is a good thing. If you want shared pointers without thread safe reference counts you can just do:
template<typename T> using st_shared_ptr = __shared_ptr<T, __gnu_cxx::_S_single>;
with gcc so its not like it is a big issue, You only pay for what you use, when you use it.
I don't suffer from insanity, I'm enjoying every minute of it.
The voices in my head may not be real, but they have some good ideas!

#35 Trienco   Crossbones+   -  Reputation: 2224

Like
0Likes
Like

Posted 05 April 2012 - 02:21 PM

Yeah I mean if two threads are reading/writing the shared_ptr object itself (i.e. changing the ref-counter that it points to). If there's only one reference, and then two threads try to simultaneously


So the problem wouldn't be two threads modifying the ref-counter, but two threads doing so through the same shared_ptr? In that case everything would still be the way I remember it. The reference counter is "thread safe", but not the actual shared_ptr or data behind it.

One consequence of the issue you describe is that new shared_ptr should always be created and passed on by an existing owner and that any one ownership should be limited to a single thread. Quite a few implicit rules for something that's supposed to make things easier.
f@dzhttp://festini.device-zero.de

#36 GorbGorb   Members   -  Reputation: 112

Like
0Likes
Like

Posted 05 April 2012 - 03:55 PM

boost::shared_ptr has the same thread-safety as a normal pointer.
You're not allowed to do

Thread 1: a = null;
Thread 2: shared_ptr<T> b = a;

with normal pointers either.

#37 Hodgman   Moderators   -  Reputation: 31920

Like
0Likes
Like

Posted 05 April 2012 - 08:31 PM

boost::shared_ptr has the same thread-safety as a normal pointer.
You're not allowed to do

Thread 1: a = null;
Thread 2: shared_ptr<T> b = a;

with normal pointers either.

Yeah, my point was that boost's shared_ptrs do have the same lack of "thread-safety" guarantees as raw pointers, but boost's shared_ptr also does make use of slow atomic operations (cache-line mutexes) in order to make them "thread safe"... while still being as "thread unsafe" as raw pointers?

Also, shared_ptr is worse than regular pointers in the above case. With a regular pointer (on most CPUs), b will either be equal to the original value of a, or it will be equal to null. With shared_ptrs, the above code will also perform either of those two possibilities, but there's also a 3rd possibility which is an access-violation/segfault crash when incrementing the reference counter.

Isn't it far better to have the language provide safe and easy to use tools out of the box and let the programmer develop their own faster, unsafe versions if needed than to do things the other way around though ?

I'd say the standard library should provide generic tools out of the box that can be used in as many different ways as possible, as this is what the STL/SC++L has always tried to do.
A switch to choose the level of "thread safety" provided would be nice, such as:

If you want shared pointers without thread safe reference counts you can just do: ... with gcc so its not like it is a big issue, You only pay for what you use, when you use it

That's neat - do you know if the official C++11 spec has a similar feature, or is this going to be a compiler-specific thing?

Defining ... BOOST_SP_DISABLE_THREADS will disable it for shared_ptr.

That's handy, but kind of useless if other parts of your code base do want the default shared_ptr behaviour. The above template argument implementation is much more usable.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS