I wrote in section 1.9 "shared_ptr thread security" of Linux multi-thread Server programming: Using muduo C ++ network library:
(Shared_ptr) the reference count itself is secure and lockless, but the read and write operations on the object are not. Because shared_ptr has two data members, read and write operations cannot be atomic. According to the document (http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), the thread security level of shared_ptr is the same as the built-in type, standard library container, std: string, that is:
• A shared_ptr object entity can be read by multiple threads at the same time (document Example 1 );
• Two shared_ptr object entities can be written by two threads at the same time (Example 2). The "structure" is a write operation;
• If you want to read and write the same shared_ptr object from multiple threads, You need to lock the object (Example 3 ~ 5 ).
Note that the above is the thread security level of the shared_ptr object itself, not the thread security level of the object it manages.
The following article (p.18) describes how to effectively unlock locks. This article will analyze why "because shared_ptr has two data members, read/write operations cannot be atomic" makes it necessary to lock multiple threads to read and write the same shared_ptr object. In my opinion, the obvious conclusion seems that some people have doubts, which will lead to disastrous consequences. It is worth writing this article. This article uses boost: shared_ptr as an example, which may be slightly different from std: shared_ptr.
Shared_ptr Data Structure
Shared_ptr is a reference counting smart pointer. Almost all implementations use a count value (count) on the heap) (in theory, there is also a method of circular linked list, but there is no instance ). Specifically, shared_ptr <Foo> contains two members: one is the pointer ptr pointing to Foo, and the other is the ref_count pointer (its type is not necessarily the original pointer, it may be the class type, but it does not affect the discussion here), pointing to the ref_count object on the stack. The ref_count object has multiple members, as shown in Data Structure 1. deleter and allocator are optional.
Figure 1: shared_ptr data structure.
In order to simplify and highlight the focus, only the values of use_count are shown below:
The above is the memory data structure corresponding to shared_ptr <Foo> x (new Foo.
If you execute shared_ptr <Foo> y = x;, the corresponding data structure is as follows.
However, y = x involves copying two members. The two copies do not occur simultaneously (atomic.
Step 1: copy the ptr pointer:
In step 2, copy the ref_count pointer to add 1 to the reference count:
The sequence of steps 1 and 2 is related to the implementation (so the direction of y. ptr is not drawn in step 2.
Since y = x has two steps, without mutex protection, race condition exists in multithreading.
Race condition that may occur in multi-thread unprotected read/write shared_ptr
Consider a simple scenario where there are three shared_ptr <Foo> objects x, g, and n:
Shared_ptr <Foo> g (new Foo); // shared_ptrshared_ptr <Foo> x shared between threads; // shared_ptr, local variable of thread A <Foo> n (new Foo ); // local variable of thread B
At the beginning, everything went well.
Thread A executes x = g; (that is, read g). The following steps are completed: 1. Then thread B is switched.
At the same time, programming B to execute g = n; (that is, write g), the two steps are completed together.
Step 1:
Step 2:
This is because the Foo1 object has been destroyed and x. ptr has become an empty hanging pointer!
Return to thread A and complete Step 2:
Multi-thread read/write g without protection, resulting in "x is a blank Pointer. This is the reason why multiple threads Must lock the same shared_ptr.
Of course, race condition is much more than this type. interweaving may cause other errors.
Think about race condition if the operator = Implementation of shared_ptr is to copy ref_count (step 2) and then replicate ptr (step 1?
Miscellaneous shared_ptr serves as the key of unordered_map
If you put boost: shared_ptr in unordered_set or the key used for unordered_map, be careful to degrade the hash table to a linked list. Http://stackoverflow.com/questions/6404765/c-shared-ptr-as-unordered-sets-key/12122314#12122314
Until Boost 1.47.0 is released, although unordered_set <std: shared_ptr <T> can be compiled, its hash_value is the result of the implicit conversion of shared_ptr to bool. That is to say, if the hash function is not customized, unordered _ {set/map} degrades to a linked list. Https://svn.boost.org/trac/boost/ticket/5216
Boost 1.51 adds overload in boost/functional/hash/extensions. hpp. Now, you only need to include this header file to use unordered_set safely and efficiently <std: shared_ptr>.
This is also the reason why the hash_value (const boost: shared_ptr <T> & x) function needs to be defined in the muduo examples/idleconnection example (section 7.10.2, p.255 ). Because Debian 6 Squeeze and boost versions in Ubuntu 10.04 LTS all have this bug.
Why does ref_count in Figure 1 also have a pointer to Foo?
Shared_ptr <Foo> sp (new Foo) captures the destructor of Foo when constructing the sp. In fact, shared_ptr.ptr and ref_count.ptr can be of different types (as long as there is implicit conversion between them), which is a major function of shared_ptr. In three points:
1. No virtual destructor is required;Assume that Bar is the base class of Foo, but neither Bar nor Foo has a virtual structure.
Shared_ptr <Foo> sp1 (new Foo); // The ref_count.ptr type is Foo *
Shared_ptr <Bar> sp2 = sp1; // you can assign values to enable automatic up-cast)
Sp1.reset (); // The reference count of the Foo object is reduced to 1.
Since then, sp2 can safely manage the lifecycle of the Foo object and safely and completely release the Foo, because its ref_count remembers the actual type of Foo.
2. shared_ptr <void>Attackers can point to and safely manage (destructor or prevent destructor) any object. The tie () function of muduo: net: Channel class uses this feature, to prevent premature object analysis, see section 7.15.3.
Shared_ptr <Foo> sp1 (new Foo); // The ref_count.ptr type is Foo *
Shared_ptr <void> sp2 = sp1; // values can be assigned. Foo * is automatically transformed to void *.
Sp1.reset (); // The reference count of the Foo object is reduced to 1.
After that, sp2 can still safely manage the lifecycle of the Foo object and safely and completely release the Foo object without the delete void *, because the delete operation is ref_count.ptr, not sp2.ptr.
3. Multi-inheritance.Assume that Bar is one of Foo's multiple base classes:
Shared_ptr <Foo> sp1 (new Foo );
Shared_ptr <Bar> sp2 = sp1; // in this case, sp1.ptr and sp2.ptr may point to different addresses, because the offset of Bar subobject in Foo object may not be 0.
Sp1.reset (); // at this time, the reference count of the Foo object is reduced to 1.
However, sp2 can still safely manage the lifecycle of the Foo object and safely and completely release Foo, because delete is not Bar *, but the original Foo *. In other words, sp2.ptr and ref_count.ptr may have different values (of course, their types are also different ).
Why do we try to use make_shared ()?
In order to save one memory allocation, the original shared_ptr <Foo> x (new Foo); each of Foo and ref_count needs to be allocated with one memory allocation. Now, if make_shared () is used, A large enough memory can be allocated at a time for the Foo and ref_count objects. The data structure is:
However, the parameters of the Foo constructor must be passed to make_shared (), and the latter must be passed to Foo: Foo (). This can be perfectly solved only when perfect forwarding is used in C ++ 11.