13

Does anybody know of a fully thread-safe shared_ptr implementation? E.g. boost implementation of shared_ptr is thread-safe for the targets (refcounting) and also safe for simultaneous shared_ptr instance reads, but not writes or for read/write.

(see Boost docs, examples 3, 4 and 5).

Is there a shared_ptr implementation that is fully thread-safe for shared_ptr instances?

Strange that boost docs say that:

shared_ptr objects offer the same level of thread safety as built-in types.

But if you compare an ordinary pointer (built-in type) to smart_ptr, then simultaneous write of an ordinary pointer is thread-safe, but simultaneous write to a smart_ptr isn't.

EDIT: I mean a lock-free implementation on x86 architecture.

EDIT2: An example use case for such a smart pointer would be where there are a number of worker threads which update a global shared_ptr with a their current work item and a monitor thread that takes random samples of the work items. The shared-ptr would own the work item until another work item pointer is assigned to it (thereby destroying the previous work item). The monitor would get ownership of the work item (thereby preventing the work item to be destroyed) by assigning it to its own shared-ptr. It can be done with XCHG and manual deletion, but would be nice if a shared-ptr could do it.

Another example is where the global shared-ptr holds a "processor", and is assigned by some thread, and used by some other thread. When the "user" thread sees that the processor shard-ptr is NULL, it uses some alternative logic to do the processing. If it's not NULL, it prevents the processor from being destroyed by assigning it to its own shared-ptr.

Piotr Dobrogost
  • 41,292
  • 40
  • 236
  • 366
Magnus Hiie
  • 494
  • 1
  • 4
  • 14
  • 5
    "simultaneous write of an ordinary pointer is thread-safe" -- are you sure about that? – OJ. Jan 14 '09 at 03:52
  • At least on x86, if the pointer is aligned correctly, the write operation is atomic. – Magnus Hiie Jan 14 '09 at 04:01
  • What about simultaneous write in one thread and delete in another? (delete is essentially a special kind of write; one that obliterates the item being pointed to). – Max Lybbert Jan 14 '09 at 08:05
  • Max: what you are describing is a simultaneous read and write. delete does not alter the value of the pointer variable itself, therefore it does not count as a write -- it is the *pointed-to* value that is (potentially) being written to (by the destructor if said exists). – j_random_hacker Jan 15 '09 at 11:45
  • j_random_hacker: Good point. – Max Lybbert Jan 15 '09 at 18:26
  • 1
    "_the write operation is atomic_" That doesn't make it "thread safe" in the common sense. – curiousguy Oct 08 '11 at 14:25

9 Answers9

18

Adding the necessary barriers for such a fully thread-safe shared_ptr implementation would likely impact performance. Consider the following race (note: pseudocode abounds):

Thread 1: global_ptr = A;

Thread 2: global_ptr = B;

Thread 3: local_ptr = global_ptr;

If we break this down into its constituent operations:

Thread 1:

A.refcnt++;
tmp_ptr = exchange(global_ptr, A);
if (!--tmp_ptr.refcnt) delete tmp_ptr;

Thread 2:

B.refcnt++;
tmp_ptr = exchange(global_ptr, B);
if (!--tmp_ptr.refcnt) delete tmp_ptr;

Thread 3:

local_ptr = global_ptr;
local_ptr.refcnt++;

Clearly, if thread 3 reads the pointer after A's swap, then B goes and deletes it before the reference count can be incremented, bad things will happen.

To handle this, we need a dummy value to be used while thread 3 is doing the refcnt update: (note: compare_exchange(variable, expected, new) atomically replaces the value in variable with new if it's currently equal to new, then returns true if it did so successfully)

Thread 1:

A.refcnt++;
tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
    tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;

Thread 2:

B.refcnt++;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
    tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;

Thread 3:

tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, BAD_PTR))
    tmp_ptr = global_ptr;
local_ptr = tmp_ptr;
local_ptr.refcnt++;
global_ptr = tmp_ptr;

You've now had to add a loop, with atomics in it in the middle of your /read/ operation. This is not a good thing - it can be extremely expensive on some CPUs. What's more, you're busy-waiting as well. You can start to get clever with futexes and whatnot - but by that point you've reinvented the lock.

This cost, which has to be borne by every operation, and is very similar in nature to what a lock would give you anyway, is why you generally don't see such thread-safe shared_ptr implementations. If you need such a thing, I would recommend wrapping a mutex and shared_ptr into a convenience class to automate locking.

bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • Very good answer, thank you. That's what I was looking for, i.e. what are the implications of this. I guess your example would work without B? – Magnus Hiie Oct 01 '09 at 13:32
  • Actually, it would still break with just threads 1 and 3 - adding thread 2 was just to show that we'd already initialized it with something first. – bdonlan Oct 01 '09 at 15:58
  • "adding the barriers would impact performance" !! well, then you should know that barriers are present, right now, both in boost and std::shared (base_ref_cnt class). Because they use atomics and atomics do execute the necessary load/store flush on the cpu (mem fence). intrinsics support by compiler and atomic<> of C++11 is even worse than CPU fence, it is also a compiler fence (no reordering of load/stores passed the atomic). shared_ptr are slow, today, already. And even worse, they are not thread safe. They are only safe if you don't have weak references. – v.oddou Dec 05 '13 at 00:53
  • @curiousguy because you can't update 2 counters atomically without using a mutex. And they only use atomic swaps/inc/dec, so I sense there must be a possibility to break the internal invariants. I implemented custom `shared_ptr` 2 times and I couldn't solve this issue. Maybe the `std` implementors are geniuses though, I'll need to check how they do it one day. – v.oddou Jun 13 '18 at 02:25
  • @v.oddou Can you give a specific example of unsafe code? – curiousguy Jun 13 '18 at 03:11
  • @curiousguy your enquiry forced me to some research. I found in boost's `sp_counted_base_gcc_x86.hpp` their trick is to decouple the 2 counts: `int weak_count_; // #weak + (#shared != 0)` The key is this artificial +1 stuff. This way, the deletion of the refcount is not subject to the race condition I faced in my implementations. That's genius :) This doesn't answer your question though. But in my research I also found a perfect little article https://www.justsoftwaresolutions.co.uk/threading/why-do-we-need-atomic_shared_ptr.html. Specific examples of unsafe code: ABA problem, race conditions – v.oddou Jun 13 '18 at 10:01
2

Simultaneous write to a built-in pointer is certainly not thread safe. Consider the implications of writing to the same value with respect to memory barriers if you really want to drive yourself crazy (for instance, you could have two threads thinking the same pointer had different values).

RE: Comment - the reason built-ins aren't double deleting is because they aren't deleting at all (and the implementation of boost::shared_ptr I use wouldn't double delete, since it uses a special atomic increment and decrement, so it would only single delete, but then the result would could have the pointer from one and the ref count of the other. Or pretty much any combination of the two. It would be bad.). The statement in the boost docs is correct as it is, you get the same guarantees as you do with a built-in.

RE: EDIT2 - The first situation you are describing are very different between using built-ins and shared_ptrs. In one (XCHG and manual delete) there's no reference count; you are assuming you are the one and only owner when you do this. If using shared pointers, you are saying other threads might have ownership, which makes things far more complex. I believe it is possible with a compare-and-swap, but this would be very non-portable.

C++0x is coming out with an atomics library, which should make it much easier to write generic multi-threaded code. You'll probably have to wait till that comes out to see good cross-platform reference implementations of thread-safe smart pointers.

Todd Gardner
  • 13,313
  • 39
  • 51
  • I agree two processors could see a different value of a built-in pointer (i.e. not see updates done by another processor), but this might be acceptable to the application. I guess this is not thread-safety in the strictest sense, but it at least wouldn't crash (boost shared_ptr would double-delete). – Magnus Hiie Jan 14 '09 at 04:11
1

I don't know of such a smart pointer implementation, though I have to ask: how could this behaviour be useful? The only scenarios I can think of where you would find simultaneous pointer updates are race conditions (i.e. bugs).

This is not a criticism -- there may well be a legitimate use case, I just can't think of it. Please let me know!

Re: EDIT2 Thanks for providing a couple of scenarios. It does sound like atomic pointer writes would be useful in those situations. (One little thing: for the second example, when you wrote "If it's not NULL, it prevents the processor from being destroyed by assigning it to its own shared-ptr", I hope you meant that you assign the global shared pointer to the local shared pointer first then check whether the local shared pointer is NULL -- the way you described it is prone to a race condition where the global shared pointer becomes NULL after you test for it and before you assign it to the local one.)

j_random_hacker
  • 50,331
  • 10
  • 105
  • 169
  • "I hope you meant that you assign the global shared pointer to the local shared pointer first" -- Yes, I should have used better wording – Magnus Hiie Jan 21 '09 at 11:23
0

You can use this implementation Atomic Reference Counting Pointers to at least implement the Reference Counting mechanism.

Rody
  • 9
  • 1
-1

I dont think this so easy, it is not enough to wrap your sh_ptr classes with a CS. It is true that if you maintain one single CS for all shared pointers it can ensure to avoid mutual the access and deletion of sh_ptr objects among different threads. But this would be terrible, one CS object for every shared pointer would be a real bottleneck. It would be suitable if every wrappable new ptr -s have different CS s' but this way we should create our CS dinamically, and ensure the copy ctors of sh_ptr classes to transmit this shared Cs. Now we arrived to the same problem: who quaranties that this Cs ptr is already deleted or not. We can be a little more smarty with volatile m_bReleased flags per instance but this way we cannot stuck the safety gaps between checking the flag and using the shared Cs. I can't see completely safe resolution for this problem. Maybe that terrible global Cs would be the minor bad as killing the app. (sorry for my english)

-1

This may not be exactly what you want, but the boost::atomic documentation provides an example on how to use an atomic counter with intrusive_ptr. intrusive_ptr is one of the Boost smart pointers, it does "intrusive reference counting", which means the counter is "embedded" in the target instead of providing by the smart pointer.

Boost atomic Usage Examples:

http://www.boost.org/doc/html/atomic/usage_examples.html

-1

Your compiler may already provide the thread safe smart pointers in the newer C++ Standards. I believe TBB is planning on adding a smart pointer, but I don't think it's been included yet. You may be able to use one of TBB's thread-safe containers, though.

Max Lybbert
  • 19,717
  • 4
  • 46
  • 69
-1

You can easily do this by including a mutex object with every shared pointer, and wrapping increment/decrement commands with the lock.

Unknown
  • 45,913
  • 27
  • 138
  • 182
-3

In my opinion, the easiest solution is to use an intrusive_ptr with a few minor (but necessary) modifications.

I shared my implementation below:

http://www.philten.com/boost-smartptr-mt/

GraphicsMuncher
  • 4,583
  • 4
  • 35
  • 50
  • 2
    Unfortunately your code isn't thread safe either. In `intrusive_ptr_release()`, the thread can be preempted after the condition of the `if` has evaluated but before the `delete`. Another thread could then for instance call `intrusive_ptr_add_ref()` and `intrusive_ptr_release()` and delete the ptr in front of the original thread who then continues as if nothing happened and tries to delete the ptr *again*. – Andreas Magnusson Oct 24 '12 at 12:12