Thread Safety of shared_ptr
Hello,
I wish to clarify a point on the thread-safety of shared_ptr. I have
read the boost documentation on shared_ptr concerning Thread-Safety and
amongst other things it gives this example (see
http://www.boost.org/doc/libs/1_46_1/libs/smart_ptr/shared_ptr.htm for
the full set of examples):
shared_ptr<int> p(new int(42));
//--- Example 1 ---
// thread A
shared_ptr<int> p2(p); // reads p
// thread B
shared_ptr<int> p3(p); // OK, multiple reads are safe
Kevin Frey wrote:
Hello,
I wish to clarify a point on the thread-safety of shared_ptr. [...]
//--- Example 4 --- // thread A p3 = p2; // reads p2, writes p3
// thread B // p2 goes out of scope: undefined, the destructor is considered a "write access"
This example just says that you can't make a copy of a destroyed shared_ptr, or one that is in the process of being destroyed. In principle, it has nothing to do with reference counts. Doing something with a destroyed object of any type is undefined behavior in C++.
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 On Monday, May 02, 2011, Peter Dimov wrote:
// thread B // p2 goes out of scope: undefined, the destructor is considered a
"write access"
This example just says that you can't make a copy of a destroyed shared_ptr, or one that is in the process of being destroyed. In principle, it has nothing to do with reference counts. Doing something with a destroyed object of any type is undefined behavior in C++.
It looks like the example isn't quite right though, as p2 was declared in thread A so it can't go out of scope in thread B. -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) iEYEARECAAYFAk2+qzEACgkQ5vihyNWuA4WHUgCePic0zbvdEHuv97Y7nnFOF6LP sncAnArkgbD3wXZjRlmxGFY7X+KrIix7 =VN8Y -----END PGP SIGNATURE-----
On Mon, May 2, 2011 at 3:01 PM, Frank Mori Hess
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
On Monday, May 02, 2011, Peter Dimov wrote:
// thread B // p2 goes out of scope: undefined, the destructor is considered a
"write access"
This example just says that you can't make a copy of a destroyed shared_ptr, or one that is in the process of being destroyed. In principle, it has nothing to do with reference counts. Doing something with a destroyed object of any type is undefined behavior in C++.
It looks like the example isn't quite right though, as p2 was declared in thread A so it can't go out of scope in thread B.
It still can happen if p2 is a weak_ptr in thread "A" and the corresponding shared_ptr goes out of scope in thread "B". Best Regards, Ovanes
On Mon, May 2, 2011 at 4:23 PM, Ovanes Markarian
On Mon, May 2, 2011 at 3:01 PM, Frank Mori Hess
wrote: -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
On Monday, May 02, 2011, Peter Dimov wrote:
// thread B // p2 goes out of scope: undefined, the destructor is considered a
"write access"
This example just says that you can't make a copy of a destroyed shared_ptr, or one that is in the process of being destroyed. In principle, it has nothing to do with reference counts. Doing something with a destroyed object of any type is undefined behavior in C++.
It looks like the example isn't quite right though, as p2 was declared in thread A so it can't go out of scope in thread B.
It still can happen if p2 is a weak_ptr in thread "A" and the corresponding shared_ptr goes out of scope in thread "B".
Best Regards, Ovanes
Actually, I am not sure with this statement. Destructor is only called if shared count reaches zero. I don't think that this can happen, that a dtor is called in one thread and the other thread is still able to increment the shared count even from the weak_ptr. I remember reading weak_ptr's source and saw that it is also protected again such a case. Thanks, Ovanes
Actually, I am not sure with this statement. Destructor is only called if shared count reaches zero. I don't think that this can happen, that a dtor is called in one thread and the other thread is still able to increment the shared count even from the weak_ptr. I remember reading weak_ptr's source and saw that it is also protected again such a case.
I have taken the example at face value, but in hindsight the examples could be a lot better because there is no context to describe the circumstances of how the shared_ptrs are instantiated. For example, is p allocated globally, and p2 & p3 allocated at the beginning of their respective threads? If so, example 4 becomes meaningless because Thread B cannot force p2 out of scope since it would only go out of scope once thread A exits, since thread A controls the scope of p2. If thread B were to force p2 "out of scope" then p2 would have to be a shared_ptr< >*, dynamically allocated., and subsequently deleted by Thread B. But it is not represented that way. Unfortunately none of this helps solve my original question, which given the direction this thread is proceeding, I will re-state in pseudo-code that approximates the *actual* situation occurring in my program: Earlier on: shared_ptr< Session >* sp_Session = new shared_ptr< Session >( new Session ); Dependent_Class* dep = new Dependent_Class( shared_ptr< Session >& shared_ptr_session ); In Dependent_Object::Dependent_Object( ) this->sp_Session = new shared_ptr< Session >( shared_ptr_session ); Now, simultaneously: Thread A: Dependent_Class* dep2 = new Dependent_Class( shared_ptr< Session>& shared_ptr_session ); In Dependent_Object::Dependent_Object( ) this->sp_Session = new shared_ptr< Session >( shared_ptr_session ); Thread B: In Dependent_Class::~Dependent_Class( ): // the cleanup of dep delete this->sp_Session; So, we have: 1. A shared object (pointed to by a shared_ptr) that is presently in existence with a reference count of at least 2. 2. One thread A creating another shared_ptr to the shared object, attempting to increment the reference count. 3. Another thread B simultaneously destroying a *different* shared_ptr which is connected to the same underlying shared object, decrementing the reference count. Note that the shared object is *not being destroyed* here, just the reference counts are being manipulated. Will shared_ptr handle this situation correctly? Thanks Kevin
On 05/02/2011 05:46 PM, Kevin Frey wrote:
Unfortunately none of this helps solve my original question, which given the direction this thread is proceeding, I will re-state in pseudo-code that approximates the **actual** situation occurring in my program:
Earlier on:
shared_ptr< Session >* sp_Session = new shared_ptr< Session >( new Session );
Whoa! ... This isn't how shared pointers works. You want: shared_ptr< Session > sp_Session = shared_ptr< Session >( new Session );
Dependent_Class* dep = new Dependent_Class( shared_ptr< Session >& shared_ptr_session );
In Dependent_Object::Dependent_Object( )
this->sp_Session = new shared_ptr< Session >( shared_ptr_session );
Now, simultaneously:
And... you don't want to be passing them around via reference. That completely defeats the purpose.
So, we have:
1. A shared object (pointed to by a shared_ptr) that is presently in existence with a reference count of at least 2.
2. One thread A creating another shared_ptr to the shared object, attempting to increment the reference count.
3. Another thread B simultaneously destroying a **different** shared_ptr which is connected to the same underlying shared object, decrementing the reference count.
Note that the shared object is **not being destroyed** here, just the reference counts are being manipulated.
Will shared_ptr handle this situation correctly?
It is difficult to answer you actual concerns given the way you are presenting the usage of shared_ptr. Take a closer look at the shared_ptr docs and a few examples to get a handle on *how* the utility is used. A general answer to your question is that reference counting is atomic with shared_ptr. See if you can re-formulate you question with proper usage so we can better understand what you are wanting to do. michael -- In the Sacramento/Folsom area? ** Profesional C++ Training mid-May ** http://www.objectmodelingdesigns.com/boostcon_deal.html Michael Caisse Object Modeling Designs www.objectmodelingdesigns.com
Whoa! ... This isn't how shared pointers works. You want:
shared_ptr< Session > sp_Session = shared_ptr< Session >( new Session );
No, I don't, actually. I have stated what I am doing. What I have not stated is *why* I am doing it. I have deliberately not stated that because I didn't want the discussion to go off on a tangent. There is nothing wrong with having a dynamically allocated shared_ptr, and no more or less wrong than having a dynamically allocated string or vector or map. My shared_ptr is within a wrapper class and you are basically implying that the only way I can use the shared_ptr is via composition, not aggregation. I will agree that composition is the preferred approach, but I can't do that, so my pseudo-code reflects that. So at the risk of complicating the discussion even further, the reason I *must* use aggregation is because the code in question is part of a hybrid C++/CLI (.NET) implementation. A C++/CLI class *cannot* use composition of a native type such as shared_ptr. So I *must* use aggregation - that's just the way it is. But my question is about boost::shared_ptr, and the context I am using it is irrelevant to my question So, having now let the cat out of the bag, here's the problem domain. My .NET C++/CLI classes, which are garbage-collected, managed classes, have ownership of native, unmanaged classes. I have three native classes A, B, C, and three corresponding garbage-collected classes GA, GB, and GC. The garbage-collected classes exist to allow my C++ classes to be used from a .NET application without requiring a full rewrite of the native C++ classes using a .NET language. For the uninitiated, .NET has a concept called a finaliser which exists explicitly to clean up unmanaged resources prior to garbage collection reclaiming the object. Garbage collection is inherently non-deterministic in nature (ie. GC collected object do not have a defined point where they go "out of scope" like conventional C++), but my native classes have a dependency sequence amongst them. In other words, my native objects can be classified as Level 0, Level 1, and Level 2, such that all Level 2 objects must be cleaned up before the Level 1 object on which it depends, and so on. Since I cannot control the order of garbage collection, I must ensure that the native parent objects do not get cleaned up before the child objects. I am using shared_ptr to ensure the native objects are destroyed at the correct time, irrespective of the order in which the garbage-collected objects are destroyed. Also, let's not get too hung up on the pseudo-code. It is *pseudo-code*. It is to illustrate a point. My real code does not pass a reference to a shared_ptr - it actually passes a reference to a containing object which itself "owns" the shared_ptr, and gets it from that object. But there is nothing fundamentally wrong with passing a reference to a shared_ptr. It doesn't "completely defeat the purpose" at all. Why bother constructing/destroying a temporary object? That's just inefficient. I use shared_ptr when I have a situation where I don't know who is going to be the last to clean-up an object, and I genuinely need to share the same object. The purpose of the question is to question the thread-safety aspects of shared_ptr in the context of a simultaneous increment/decrement of the ref counts by two different threads. Nothing more, nothing less.
Kevin Frey wrote:
2. One thread A creating another shared_ptr to the shared object, attempting to increment the reference count.
3. Another thread B simultaneously destroying a *different* shared_ptr which is connected to the same underlying shared object, decrementing the reference count.
This is OK. Manipulating different shared_ptr instances in different threads is fine. The example states that you can't, in thread B, destroy the shared_ptr which thread A copies (*sp_Session in your code). Destroying other shared_ptr instances is allowed, regardless of whether or with what they share ownership. The thread safety rules work on shared_ptr instances, not on their pointees. You can replace shared_ptr with std::string (or even int) in the examples and they will be equally valid (provided that reset is replaced with assignment). (Although I admit that the variable names are used somewhat loosely.)
Can we put that one sentence in the documentation in bold? Even if it is technically redundant, that's the phrasing that made it sink in for me. -- I agree. There is a statement noting how shared_ptr has the same thread-safety as a built-in type, but it is too abstract (for me anyway) to really interpret what that really means. Thinking of shared_ptr "like a string", which is much easier to picture the thread safety implications, was like turning on a lightbulb for me. It also corresponds to what I considered "intuitive" behaviour - which is what I was trying to confirm.
On Tue, May 3, 2011 at 2:18 PM, Peter Dimov
Kevin Frey wrote:
2. One thread A creating another shared_ptr to the shared object,
attempting to increment the reference count.
3. Another thread B simultaneously destroying a *different* shared_ptr which is connected to the same underlying shared object, decrementing the reference count.
This is OK. Manipulating different shared_ptr instances in different threads is fine. The example states that you can't, in thread B, destroy the shared_ptr which thread A copies (*sp_Session in your code).
[...]
Peter, can you please give an example how that can happen, given the shared_counter is atomic. I thought the counter is first incremented than the rest of the machinery deals with pointer copying etc. and in case of underlying object destruction, the counter is first decremented and than the object is destroyed. How is that possible, that shared_counter reaches 0, destructor is called and afterwards the underlying pointer will be assigned to the new shared instance? With Kind Regards, Ovanes
Peter, can you please give an example how that can happen, given the shared_counter is atomic. I thought the counter is first incremented than the rest of the machinery deals with pointer copying etc. and in case of underlying object destruction, the counter is first decremented and than the object is destroyed. How is that possible, that shared_counter reaches 0, destructor is called and afterwards the underlying pointer will be assigned to the new shared instance? With Kind Regards, Ovanes --- The main point that comes to mind is because the example I give is specifically constructed to point out that the destruction of the object in the second thread is a refcount decrement only, and does not result in destruction of the shared object itself, only the shared_ptr instance. Hence the potential race condition does not apply to my example. The actual destruction of the shared object occurs in a situation where I know there will not be a simultaneous "copy". Of course, Peter will provide his own answer.
On Tue, May 3, 2011 at 12:59 PM, Ovanes Markarian
On Tue, May 3, 2011 at 2:18 PM, Peter Dimov
wrote: Kevin Frey wrote:
2. One thread A creating another shared_ptr to the shared object, attempting to increment the reference count.
3. Another thread B simultaneously destroying a *different* shared_ptr which is connected to the same underlying shared object, decrementing the reference count.
This is OK. Manipulating different shared_ptr instances in different threads is fine. The example states that you can't, in thread B, destroy the shared_ptr which thread A copies (*sp_Session in your code).
[...]
Peter, can you please give an example how that can happen, given the shared_counter is atomic. I thought the counter is first incremented than the rest of the machinery deals with pointer copying etc. and in case of underlying object destruction, the counter is first decremented and than the object is destroyed. How is that possible, that shared_counter reaches 0, destructor is called and afterwards the underlying pointer will be assigned to the new shared instance? With Kind Regards, Ovanes
He means that while the reference count is thread safe, the pointer is not. Here's an example that will cause problems: Thread A: ptr1 = ptr2; Thread B: ptr2 = ptr; Note that ptr2 is being concurrently read and modified. Here's an example that safely modifies only the reference count. No pointers are written to concurrently: Thread A: ptr1 = ptr; Thread B: ptr2 = ptr; ptr2.reset(); -- Cory Nelson http://int64.org
On Wed, May 4, 2011 at 12:17 AM, Cory Nelson
On Tue, May 3, 2011 at 12:59 PM, Ovanes Markarian
wrote: On Tue, May 3, 2011 at 2:18 PM, Peter Dimov
wrote: Peter, can you please give an example how that can happen, given the
is atomic. I thought the counter is first incremented than the rest of
shared_counter the
machinery deals with pointer copying etc. and in case of underlying object destruction, the counter is first decremented and than the object is destroyed. How is that possible, that shared_counter reaches 0, destructor is called and afterwards the underlying pointer will be assigned to the new shared instance? With Kind Regards, Ovanes
He means that while the reference count is thread safe, the pointer is not. Here's an example that will cause problems:
Thread A: ptr1 = ptr2; Thread B: ptr2 = ptr;
Note that ptr2 is being concurrently read and modified.
Oh, I see. But this looks for me as the wrong usage of shared_ptr. ptr2 should not be passed to Thread A by reference, but should be copied into the Thead A's context. Otherwise I can't imagine how this can happen. The same can happen in the single threaded environment if one uses references to shared_ptr instances. Best Regards, Ovanes
This is OK. Manipulating different shared_ptr instances in different threads is fine. The example states that you can't, in thread B, destroy the shared_ptr which thread A copies (*sp_Session in your code). Destroying other shared_ptr instances is allowed, regardless of whether or with what they share ownership. The thread safety rules work on shared_ptr instances, not on their pointees. You can replace shared_ptr with std::string (or even int) in the examples and they will be equally valid (provided that reset is replaced with assignment). (Although I admit that the variable names are used somewhat loosely.)
Okay! Thank-you. Now I am getting an understanding.
participants (7)
-
Cory Nelson
-
Frank Mori Hess
-
Kevin Frey
-
Marsh Ray
-
Michael Caisse
-
Ovanes Markarian
-
Peter Dimov