C++ development: why multithreaded read and write shared_ptr to lock the details

  • 2020-04-01 21:40:39
  • OfStack

I wrote in section 1.9, "on shared_ptr thread safety again," of "Linux multithreaded server programming: using the muduo C++ network library" :

The reference count itself (shared_ptr) is safe and unlocked, but the read and write of the object is not, because shared_ptr has two data members and the read and write operations cannot be atomized. According to the document (http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), from the thread safety level and built-in types, standard library containers, STD: : string, that is:

The & # 8226; A shared_ptr object entity can be read by multiple threads simultaneously (document example 1).

The & # 8226; Two shared_ptr object entities can be written simultaneously by two threads (example 2).

The & # 8226; If you want to read and write the same shared_ptr object from multiple threads, you need to lock it (examples 3-5).

Note that this is the thread-safety level of the shared_ptr object itself, not the thread-safety level of the objects it manages.

The following (p.18) describes how to lock and unlock efficiently. This article examines why multiple threads need to be locked to read and write the same shared_ptr object "because shared_ptr has two data members and cannot atomize read and write operations." This conclusion, which seems obvious to me, also seems to be in doubt. It would lead to disastrous consequences and is worth writing about. This article takes boost::shared_ptr as an example, which may be slightly different from STD ::shared_ptr.

The data structure for shared_ptr

Shared_ptr is a reference counting smart pointer, and almost all implementations use the method of placing a count on the heap (there is also a method of circular linked lists in theory, but there are no instances). Specifically, shared_ptr< Foo> Contains two members, one is PTR, a pointer to Foo, and the other is a ref_count pointer (which is not necessarily of the original type, but may be of class type, but does not affect the discussion here) to a ref_count object on the heap. The ref_count object has multiple members, and the specific data structure is shown in figure 1, where deleter and allocator are optional.

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp0 BORDER = 0 Alt = sp0 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114946.png "width = 361 height = 247 >

Figure 1: the data structure for shared_ptr.

To simplify and highlight, only the value of use_count is drawn later:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp1 BORDER = 0 Alt = sp1 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114947.png "width = 225 height = 95 >

The above is from < Foo> X (new Foo); The corresponding in-memory data structure.

If you execute shared_ptr< Foo> Y = x; The corresponding data structure is as follows.

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp2 BORDER = 0 Alt = sp2 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114948.png "width = 369 height = 95 >

But y=x involves the replication of two members, and these two copies do not occur at the same time.

Intermediate step 1, copy the PTR pointer:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp3 BORDER = 0 Alt = sp3 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114949.png "width = 369 height = 95 >

Intermediate step 2, copy the ref_count pointer, causing the reference count to add 1:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp4 BORDER = 0 Alt = sp4 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114950.png "width = 369 height = 95 >

The sequence of steps 1 and 2 is implementation-related (so there is no y.p.tr in step 2), and everything I've seen is 1 followed by 2.

Since y=x has two steps, if there is no mutex protection, then there is a race condition in multithreading.

Multithreaded unprotected read/write shared_ptr race condition that may occur

Consider a simple scenario with three shared_ptr; Foo> Objects x, g and n:

From < Foo> G (new Foo); // shared_ptrshared_ptr< Shared between threads; Foo> X; // thread A local variable shared_ptr< Foo> N (new Foo); // local variable of thread B

In the beginning, each set his own course.

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp5 BORDER = 0 Alt = sp5 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114951.png "width = 657 height = 95 >

Thread A executes x = g; (read g), which completes step 1, but not step 2. At this point, we switch to B thread.

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp6 BORDER = 0 Alt = sp6 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114952.png "width = 657 height = 95 >

At the same time, program B to execute g = n; The two steps are done together.

First step 1:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp7 BORDER = 0 Alt = sp7 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114953.png "width = 657 height = 95 >

Step 2:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp8 BORDER = 0 Alt = sp8 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114954.png "width = 657 height = 95 >

This is Foo1 object has been destroyed, x.tr has become a dangling pointer!

Finally, go back to thread A and complete step 2:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp9 BORDER = 0 Alt = sp9 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114955.png "width = 657 height = 109 >

Multiple threads read and write g without protection, resulting in the result that "x is a dangling pointer". This is why multiple threads reading and writing to the same shared_ptr must be locked.

Of course, there is more to the race condition than that. Other threads interweaving can cause other errors.

Consider, if the operator= implementation of shared_ptr were to copy ref_count (step 2) and then PTR (step 1), what race conditions would there be?

Miscellaneous shared_ptr as the key of unordered_map

If you put boost::shared_ptr into unordered_set, or the key for unordered_map, be careful that the hash table degenerates into a linked list. http://stackoverflow.com/questions/6404765/c-shared-ptr-as-unordered-sets-key/12122314#12122314

Until Boost 1.47.0 was released, unordered_set< STD: : from < T> > Its hash_value is the result of an implicit conversion of shared_ptr to a bool, although it can be compiled. That is, if you don't customize the hash function, unordered_{set/map} degenerates into a linked list. https://svn.boost.org/trac/boost/ticket/5216

Boost 1.51 in Boost/functional/hash/extensions of HPP has increased about overloading, now only include the header file can be used safely and efficiently unordered_set < STD: : from > .

This is also muduo's examples/idleconnection example to define its own hash_value(const boost::shared_ptr< T> &x) function (book 7.10.2, p.255). Because the boost versions of Debian 6 squeezers and Ubuntu 10.04 LTS all have this bug.

Why does ref_count in figure 1 also have a pointer to Foo?

From < Foo> Sp (new Foo) captures the destructive behavior of Foo when constructing sp. In fact, shared_ptr.ptr and ref_count.ptr can be of different types (as long as there is an implicit conversion between them), which is a great feature of shared_ptr. At three o 'clock:

1. No virtual destruction is required; Assume that Bar is a base class for Foo, but neither Bar nor Foo has an imaginary destructor.

From < Foo> Sp1 (new Foo); // ref_count.ptr is of type Foo*

From < Bar> Sp2 = sp1; // can assign value, auto up cast

Sp1. The reset (); // at this point the reference count of the Foo object drops to 1

Sp2 can then safely manage the lifetime of the Foo object and safely release Foo in its entirety, because its ref_count remembers the actual type of Foo.

2. From < Void> Can point to and safely manage (destruct or prevent destruct) any object; Muduo ::net::Channel class's tie() function USES this feature to prevent premature object destruction, as shown in section 7.15.3.

From < Foo> Sp1 (new Foo); // ref_count.ptr is of type Foo*

From < Void> Sp2 = sp1; // can be assigned, Foo* automatically converts to void*

Sp1. The reset (); // at this point the reference count of the Foo object drops to 1

After that sp2 can still safely manage the lifetime of the Foo object and safely release Foo in its entirety without the delete void star situation, because the delete is ref_count. PTR, not sp2.ptr.

3. Multiple inheritance. Assuming that Bar is one of Foo's multiple base classes, then:

From < Foo> Sp1 (new Foo);

From < Bar> Sp2 = sp1; // at this point, sp1.ptr and sp2.ptr may point to different addresses, because the offset of Bar subobject in Foo object may not be 0.

Sp1. The reset (); // at this point the reference count of the Foo object drops to 1

But sp2 can still safely manage the lifetime of the Foo object and safely release Foo in its entirety, because instead of Bar*, the original Foo* is deleted. In other words, sp2.ptr and ref_count.ptr may have different values (and of different types).

Why make as much as possible with make_shared()?

To save a memory allocation, shared_ptr; Foo> X (new Foo); You need to allocate Foo and ref_count once each, and now with make_shared(), you can allocate enough memory at a time to accommodate Foo and ref_count objects. The data structure is:

The < img style = "border = 0 border - BOTTOM: 0 px; BORDER - LEFT: 0 px; DISPLAY: inline. BORDER - TOP: 0 px; BORDER - RIGHT: 0 px "title = sp10 BORDER = 0 Alt = sp10 SRC =" / / files.jb51.net/file_images/article/201304/2013042814114956.png "width = 347 height = 211 >

However, Foo's constructor parameter is passed to make_shared(), which in turn is passed to Foo::Foo(), which is only perfectly resolved in C++11 with perfect forwarding.


Related articles: