volatile能保持线程安全吗_shared_ptr是线程安全的吗?

本文详细分析了C++中`shared_ptr`在多线程环境下的线程安全特性。虽然`shared_ptr`的引用计数增加和减少是线程安全的,但在不同线程间对同一`shared_ptr`对象进行读写操作时仍需加锁以避免竞态条件。通过代码示例和内存数据结构解释了不加锁可能导致的空悬指针问题。
摘要由CSDN通过智能技术生成
7307e1753aff38393fca33005c3ad08c.png请看下面一段代码 2a14b18ca15ee1bce4a2298d85f62b34.png
#include#include#include #include #include #include using namespace std;shared_ptr global_instance = make_shared(0);std::mutex g_i_mutex;void thread_fcn(){    //std::lock_guard<:mutex> lock(g_i_mutex);     shared_ptr local = global_instance; // thread-safe    for(int i = 0; i < 100000000; i++)    {        //*global_instance = *global_instance + 1;        *local = *local + 1;    }}//g++ -std=c++11 thead_01.cpp -lpthread int main(int argc, char** argv){    thread thread1(thread_fcn);    thread thread2(thread_fcn);    thread1.join();    thread2.join();    cout << "*global_instance is " << *global_instance << endl;    return 0;}

思考10秒

  • 执行结果:

02de0d5ac6c032a71b85ac9b756aa08b.png

  • 预期结果:

 *global_instance is 200000000

画外音:

执行结果 不是预期结果,肯定不是线程安全的。

为什么还说内置安全的。

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

0497abe18b31900a36f325f0b19c8d69.png

3ad65ed97d9b9f179b9e904def7462d0.png查看Effective_Modern_C++. f9698426b31f6a8bb5a21355a6e80889.png

f6854da08d2a7e448227dd17408fadab.png

17ac9c919bc4c4cab8e4a14f97772258.png

abfdc6b894954887df56ee1a85a18e7f.png

意思是说:

  • shared_ptr的引用计数本身是安全且无锁的。

  • 多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的

bb11ab336316808642efe6171da07d86.png

画外音

智能指针有2个成员,一个是引用计数是原子的,另外一个原始指针 不是

综合来说 就不是

3ad65ed97d9b9f179b9e904def7462d0.png继续查看文档shared_ptr_thread_safety f9698426b31f6a8bb5a21355a6e80889.png
  • Examples:  引用计数改变 原子操作 安全

shared_ptr p(new int(42));

Code Example 4. Reading a shared_ptr from two threads

// thread A
shared_ptr p2(p); // reads p
// thread B
shared_ptr p3(p); // OK, multiple reads are safe
  • Code Example 5. Writing different shared_ptr instances from two threads 引用计数改变 原子操作 安全

// thread A
p.reset(new int(1912)); // writes p

// thread B
p2.reset(); // OK, writes p2
  • Code Example 6. Reading and writing a shared_ptr from two threads

// thread A
p = p3; // reads p3, writes p

// thread B
p3.reset(); // writes p3; undefined, simultaneous read/write
  • Code Example 7. Reading and destroying a shared_ptr from two threads

// thread A
p3 = p2; // reads p2, writes p3

// thread B
// p2 goes out of scope: undefined, the destructor is considered a "write access"
  • Code Example 8. Writing a shared_ptr from two threads

// thread A
p3.reset(new int(1));

// thread B
p3.reset(new int(2)); // undefined, multiple writes

Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.

结论:多个线程同时读同一个shared_ptr对象是线程安全的,

但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。

这里举个例子:怎么多线程调度执行顺序的不确定性。

9fca0c74d6041f24bd695687672db130.png

3ad65ed97d9b9f179b9e904def7462d0.png为什么多线程读写 shared_ptr 要加锁? f9698426b31f6a8bb5a21355a6e80889.png

以下内容,摘自陈硕的 http://blog.csdn.net/solstice/article/details/8547547

bcf8623337853f114b8dbb5885027b15.png

1:shared_ptr 的数据结构

shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。

具体来说,shared_ptr 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。

 db46f5fe34fd2d64fa4bf486d5e39d35.png

图 1:shared_ptr 的数据结构。

为了简化并突出重点,后文只画出 use_count 的值:

96e53c32deccf42ffdae4424ff99491e.png 

以上是 shared_ptr x(new Foo); 对应的内存数据结构。

如果再执行 shared_ptr y = x; 那么对应的数据结构如下。

 ad2f7980c59a58f3d5d0df8484a9381a.png


但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。

  • 中间步骤 1,复制 ptr 指针:

 adf052d88d3ba1ecfbe8499369826f73.png

  • 中间步骤 2,复制 ref_count 指针,导致引用计数加 1:

 0d9a1a31ad6dcd178c92e5f6c126c673.png

步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),

我见过的都是先1后2。

既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。

2:多线程无保护读写 shared_ptr 可能出现的 race condition

考虑一个简单的场景,有 3 个 shared_ptr 对象 x、g、n:

shared_ptr g(new Foo1); // 线程之间共享的 shared_ptrshared_ptr x; // 线程 A 的局部变量shared_ptr n(new Foo2); // 线程 B 的局部变量-------------------------------------------线程 Ax = g; (即 read g) //代码1 :赋值指针,赋值 引用计数-------------------------------------------线程 Bg = n;//代码2 :动作A 清空原来G指向Foo1, 动作B 然后重新赋值 Foo2测试场景:线程A   智能指针x 读取Foo1,然后还重置Foo1计数。线程 B: 销毁了Foo1线程A重置计数是,foo1已经被销毁。

一开始,各安其事:

 07dc84e1b7c06d12afc0d1e5bb1a6f53.png

  •   变量 x  没有指向任何对象  

线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。

 8a4b38fc34113319360c8527a1f525de.png

  •  x 开始赋值,其中2个成员 ,第一是原始指针 FOO1,第二个应用计数没来得及完成。

同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。

先是步骤 1:

 05f9a4c5a7d8746d1dd2d3d2b46b04e4.png

  • 再是步骤 2:

 3fac92029a5d64c4b6eddf009f224e43.png

这时 Foo1 对象已经销毁,x.ptr 成了空悬指针!

  •  FOO1 因为 全局对象g重置,开始销毁

最后回到线程 A,完成步骤 2:

 92ffa388e8e92cb328b9bb91e1d35ffa.png

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。

7d042a2dbaf4d9d87a6afd2168a335af.png

  • 最后线程A 开始使用 foo1 来 执行其他操作。其实已经被销毁了。不存在 

这正是多线程读写同一个 shared_ptr 必须加锁的原因

646ce8f123e4b45b415a865c6f2a2c8e.png

3ad65ed97d9b9f179b9e904def7462d0.png查看代码 f9698426b31f6a8bb5a21355a6e80889.png
https://www.boost.org/doc/libs/1_32_0/boost/detail/shared_count.hpptemplate<class T> class shared_ptr{  T * px;                     // contained pointer  boost::detail::shared_count pn; // reference counter ,}class shared_count{private:    sp_counted_base * pi_;}

思考与行动:

1。为什么用一个类来管理另外一个指针呢

提示:

聚合关系图:

2634644ef784733667dcb7629aa703f3.png

组合关系图:

0cdc0cd88e66c773972e5972faf0d73a.png

2. 共享指针缺点

提示:

6390b5d89c7769394bc444ee5964dfcd.png

5af1fa97ec579cdcae5b83622d5a5147.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值