智能指针的线程安全

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了智能指针的线程安全相关的知识,希望对你有一定的参考价值。

参考技术A 智能指针包括一个实际数据指针和一个引用计数指针,这两个操作不是一个指令可以完成的,因此多线程环境下,势必有问题。

根据 boost官方文档 shared_ptr_thread_safety 有如下结论:

Examples:
shared_ptr<int> p(new int(42));
Code Example 4. Reading a shared_ptr from two threads,线程安全

Code Example 5. Writing different shared_ptr instances from two threads,线程安全

Code Example 6. Reading and writing a shared_ptr from two threads,线程不安全

Code Example 7. Reading and destroying a shared_ptr from two threads,线程不安全

Code Example 8. Writing a shared_ptr from two threads,线程不安全

参考 为什么多线程读写 shared_ptr 要加锁? ,假设一个shared_ptr的复制分两个步骤:

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

一开始,各安其事。

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

weak_ptr最初的引入,是为了解决shared_ptr互相引用导致的内存无法释放的问题。weak_ptr不会增加引用计数,不能直接操作对象的内存(需要先调用 lock 接口),需要和shared_ptr配套使用。

同时,通过weak_ptr获得的shared_ptr可以安全使用,因为其 lock 接口是原子性的,那么lock返回的是一个新的shared_ptr,不存在同一个shared_ptr的读写操作,除非后续又对这个新的shared_ptr又被其他线程同时读写。

智能指针是否线程安全

1.9 再论shared_ptr 的线程安全

虽然我们借shared_ptr 来实现线程安全的对象释放,但是shared_ptr 本身不是100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。根据文档11,shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

一个shared_ptr 对象实体可被多个线程同时读取;

两个shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作;

如果要从多个线程读写同一个shared_ptr 对象,那么需要加锁。

请注意,以上是shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。

要在多个线程中同时访问同一个shared_ptr,正确的做法是用mutex 保护:

  1. MutexLock mutex; // No need for ReaderWriterLock  
  2. shared_ptr<Foo> globalPtr;  
  3. // 我们的任务是把globalPtr 安全地传给doit()  
  4. void doit(const shared_ptr<Foo>& pFoo); 

globalPtr 能被多个线程看到,那么它的读写需要加锁。注意我们不必用读写锁,而只用最简单的互斥锁,这是为了性能考虑。因为临界区非常小,用互斥锁也不会阻塞并发读。

为了拷贝globalPtr,需要在读取它的时候加锁,即:

  1. void read()  
  2. {  
  3. shared_ptr<Foo> localPtr;  
  4. {  
  5. MutexLockGuard lock(mutex);  
  6. localPtr = globalPtr; // read globalPtr  
  7. }  
  8. // use localPtr since here,读写localPtr 也无须加锁  
  9. doit(localPtr);  

 

写入的时候也要加锁:

  1. void write()  
  2. {  
  3. shared_ptr<Foo> newPtr(new Foo); // 注意,对象的创建在临界区之外  
  4. {  
  5. MutexLockGuard lock(mutex);  
  6. globalPtr = newPtr; // write to globalPtr  
  7. }  
  8. // use newPtr since here,读写newPtr 无须加锁  
  9. doit(newPtr);  

 

注意到上面的read() 和write() 在临界区之外都没有再访问globalPtr,而是用了一个指向同一Foo 对象的栈上shared_ptr local copy。下面会谈到,只要有这样的local copy 存在,shared_ptr 作为函数参数传递时不必复制,用reference to const 作为参数类型即可。另外注意到上面的new Foo 是在临界区之外执行的,这种写法通常比在临界区内写globalPtr.reset(new Foo) 要好,因为缩短了临界区长度。如果要销毁对象,我们固然可以在临界区内执行globalPtr.reset(),但是这样往往会让对象析构发生在临界区以内,增加了临界区的长度。一种改进办法是像上面一样定义一个localPtr,用它在临界区内与globalPtr 交换(swap()),这样能保证把对象的销毁推迟到临界区之外。练习:在write() 函数中,globalPtr = newPtr; 这一句有可能会在临界区内销毁原来globalPtr 指向的Foo 对象,设法将销毁行为移出临界区。

 

以上是关于智能指针的线程安全的主要内容,如果未能解决你的问题,请参考以下文章

智能指针是否线程安全

网易面试题如何实现一个线程安全的shared_ptr智能指针

网易面试题如何实现一个线程安全的shared_ptr智能指针

网易面试题如何实现一个线程安全的shared_ptr智能指针

网易面试题如何实现一个线程安全的shared_ptr智能指针

图解shared_ptr共享智能指针原理分析