Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fully thread-safe shared_ptr implementation

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.

like image 275
Magnus Hiie Avatar asked Jan 14 '09 03:01

Magnus Hiie


People also ask

Is shared_ptr thread safe?

A std::shared_ptr consists of a control block and its resource. Yes, the control block is thread-safe; but no, the access to the resource is not thread-safe. That means, modifying the reference counter is an atomic operation and you have the guarantee that the resource will be deleted exactly once.

Is Make_shared thread safe?

The snippet you pulled from msdn basically means "access to the control block is thread-safe" so other shared_ptr<> instances can be created and destroyed on different threads as much as necessary. //In thread 1 local_instance = make_shared<myClass>(); This is fine.

How is shared_ptr implemented C++?

A cyclic shared_ptr chain can be broken by changing the code so that one of the references is a weak_ptr . This is done by assigning values between shared pointers and weak pointers, but a weak pointer doesn't affect the reference count. If the only pointers that point to an object are weak, the object is destroyed.

Is Weak_ptr thread safe?

Using weak_ptr and shared_ptr across threads is safe; the weak_ptr/shared_ptr objects themselves aren't thread-safe.


2 Answers

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.

like image 153
bdonlan Avatar answered Oct 25 '22 06:10

bdonlan


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.

like image 28
Todd Gardner Avatar answered Oct 25 '22 06:10

Todd Gardner