iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >shared_ptr线程安全性全面分析
  • 836
分享到

shared_ptr线程安全性全面分析

shared_ptr线程安全性 2022-11-15 23:11:08 836人浏览 八月长安
摘要

正如《STL源码剖析》所讲,“源码之前,了无秘密”。本文基于shared_ptr的源代码,提取了shared_ptr的类图和对象图,然后分析了shared_ptr如何保证文档所宣称的

正如《STL源码剖析》所讲,“源码之前,了无秘密”。本文基于shared_ptr的源代码,提取了shared_ptr的类图和对象图,然后分析了shared_ptr如何保证文档所宣称的线程安全性。本文的分析基于boost 1.52版本,编译器是VC 2010。

shared_ptr的线程安全性
boost官方文档对shared_ptr线程安全性的正式表述是:shared_ptr对象提供与内置类型相同级别的线程安全性。【shared_ptrobjects offer the same level of thread safety as built-in types.】具体是以下三点。

1. 同一个shared_ptr对象可以被多线程同时读取。【A shared_ptrinstance can be "read" (accessed using only const operations)simultaneously by multiple threads.】

2. 不同的shared_ptr对象可以被多线程同时修改(即使这些shared_ptr对象管理着同一个对象的指针)。【Different shared_ptr instances can be "written to"(accessed using mutable operations such as operator= or reset) simultaneouslyby multiple threads (even when these instances are copies, and share the samereference count underneath.) 】

3. 任何其他并发访问的结果都是无定义的。【Any other simultaneous accesses result in undefined behavior.】

第一种情况是对对象的并发读,自然是线程安全的

第二种情况下,如果两个shared_ptr对象A和B管理的是不同对象的指针,则这两个对象完全不相关,支持并发写也容易理解。但如果A和B管理的是同一个对象P的指针,则A和B需要维护一块共享的内存区域,该区域记录P指针当前的引用计数。对A和B的并发写必然涉及对该引用计数内存区的并发修改,这需要boost做额外的工作,也是本文分析的重点。

另外weak_ptr和shared_ptr紧密相关,用户可以从weak_ptr构造出shared_ptr,也可以从shared_ptr构造weak_ptr,但是weak_ptr不涉及到对象的生命周期。由于shared_ptr的线程安全性是和weak_ptr耦合在一起的,本文的分析也涉及到weak_ptr。

下面先从总体上看一下shared_ptr和weak_ptr的实现。

shared_ptr的结构图
以下是从boost源码提取出的shared_ptr和weak_ptr的类图。




我们首先忽略虚线框内的weak_ptr部分。最高层的shared_ptr就是用户直接使用的类,它提供shared_ptr的构造、复制、重置(reset函数)、解引用、比较、隐式转换为bool等功能。它包含一个指向被管理对象的指针,用来实现解引用操作,并且组合了一个shared_count对象,用来操作引用计数。

但shared_count类还不是引用计数类,它只是包含了一个指向引用计数类sp_counted_base的指针,功能上是对sp_counted_base操作的封装。shared_count对象的创建、复制和删除等操作,包含着对sp_counted_base的增加和减小引用计数的操作。

最后sp_counted_base类才保存了引用计数,并且对引用计数字段提供无保护。它也包含了一个指向被管理对象的指针,是用来删除被管理的对象的。sp_counted_base有三个派生类,分别处理用户指定Deleter和Allocator的情况:

1. sp_counted_impl_p:用户没有指定Deleter和Allocator

2. sp_counted_impl_pd:用户指定了Deleter,没有指定Allocator

3. sp_counted_impl_pda:用户指定了Deleter和 Allocator

创建指针P的第一个shared_ptr对象的时候,子对象shared_count同时被建立, shared_count根据用户提供的参数选择创建一个特定的sp_counted_base派生类对象X。之后创建的所有管理P的shared_ptr对象都指向了这个独一无二的X。

然后再看虚线框内的weak_ptr就清楚了。weak_ptr和shared_ptr基本上类似,只不过weak_ptr包含的是weak_count子对象,但weak_count和shared_count也都指向了sp_counted_base。

如果上面的文字还不够清楚,下面的代码就能说明问题。


shared_ptr<SomeObject> SP1(new SomeObject());

shared_ptr<SomeObject> SP2=SP1;

weak_ptr<SomeObject> WP1=SP1;


执行完以上代码后,内存中会创建以下对象实例,其中红色箭头表示指向引用计数对象的指针,黑色箭头表示指向被管理对象的指针。




从上面可以清楚的看出,SP1、SP2和WP1指向了同一个sp_counted_impl_p对象,这个sp_counted_impl_p对象保存引用计数,是SP1、SP2和WP1等三个对象共同操作的内存区。多线程并发修改SP1、SP2和WP1,有且只有sp_counted_impl_p对象会被并发修改,因此sp_counted_impl_p的线程安全性是shared_ptr以及weak_ptr线程安全性的关键问题。而sp_counted_impl_p的线程安全性是在其基类sp_counted_base中实现的。下面将着重分析sp_counted_base的代码。

引用计数类sp_counted_base
幸运的是,sp_counted_base的代码量很小,下面全文列出来,并添加有注释。


class sp_counted_base
{
private:
     // 禁止复制
    sp_counted_base( sp_counted_base const & );
    sp_counted_base & operator= ( sp_counted_baseconst & );

     // shared_ptr的数量
    long use_count_; 
     // weak_ptr的数量+1
    long weak_count_;     

public:
     // 唯一的一个构造函数,注意这里把两个计数都置为1
    sp_counted_base(): use_count_( 1 ), weak_count_( 1 ){    }

     // 虚基类,因此可以作为基类
    virtual ~sp_counted_base(){    }

     // 子类需要重载,用operator delete或者Deleter删除被管理的对象
    virtual void dispose() = 0;

     // 子类可以重载,用Allocator等删除当前对象
    virtual void destroy(){
        delete this;
    }

    virtual void * get_deleter( sp_typeinfo const & ti ) = 0;

     // 这个函数在根据shared_count复制shared_count的时候用到
     // 既然存在一个shared_count作为源,记为A,则只要A不释放,
     // use_count_就不会被另一个线程release()为1。
     // 另外,如果一个线程把A作为复制源,另一个线程释放A,执行结果是未定义的。
     void add_ref_copy(){
        _InterlockedIncrement( &use_count_ );
    }

     // 这个函数在根据weak_count构造shared_count的时候用到
     // 这是为了避免通过weak_count增加引用计数的时候,
     // 另外的线程却调用了release函数,清零use_count_并释放了指向的对象
    bool add_ref_lock(){
        for( ;; )
        {
            long tmp = static_cast< long const volatile& >( use_count_ );
            if( tmp == 0 ) return false;

            if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;
        }
    }

    void release(){
        if( _InterlockedDecrement( &use_count_ ) == 0 )
        {
              // use_count_从1变成0的时候,
              // 1. 释放对象
              // 2. 对weak_count_执行一次递减操作。这是因为在初始化的时候(use_count_从0变1时),weak_count初始值为1
            dispose();
            weak_release();
        }
    }

    void weak_add_ref(){
        _InterlockedIncrement( &weak_count_ );
    }

     // 递减weak_count_;且在weak_count为0的时候,把自己删除
    void weak_release(){
        if( _InterlockedDecrement( &weak_count_ ) == 0 )
        {
            destroy();
        }
    }

     // 返回引用计数。注意如果用户没有额外加锁,引用计数完全可能同时被另外的线程修改掉。
    long use_count() const{
        return static_cast<long const volatile &>( use_count_ );
    }
};

代码中的注释已经说明了一些问题,这里再重复一点:use_count_字段等于当前shared_ptr对象的数量,weak_count_字段等于当前weak_ptr对象的数量加1。

首先不考虑weak_ptr的情况。根据对shared_ptr类的代码分析(代码没有列出来,但很容易找到),shared_ptr之间的复制都是调用add_ref_copy和release函数进行的。假设两个线程分别对SP1和SP2进行操作,操作的过程无非是以下三种情况:

1. SP1和SP2都递增引用计数,即add_ref_copy被并发调用,也就是两个_InterlockedIncrement(&use_count_)并发执行,这是线程安全的。

2. SP1和SP2都递减引用计数,即release被并发调用,也就是_InterlockedDecrement(&use_count_ )并发执行,这也是线程安全的。只不过后执行的线程负责删除对象。

3.  SP1递增引用计数,调用add_ref_copy;SP2递减引用计数,调用release。由于SP1的存在,SP2的release操作无论如何都不会导致use_count_变为零,也就是说release中if语句的body永远不会被执行。因此,这种情况就化简为_InterlockedIncrement(&use_count_)和_InterlockedDecrement( &use_count_ )的并发执行,仍然是线程安全的。

然后考虑weak_ptr。如果是weak_ptr之间的操作,或者从shared_ptr构造weak_ptr,都不涉及到use_count_的操作,只需要调用weak_add_ref和weak_release来操作weak_count_。与上面的分析相同,_InterlockedIncrement和_InterlockedDecrement保证了weak_add_ref和weak_release并发操作的线程安全性。但如果存在从weak_ptr构造shared_ptr的操作,则需要考虑在构造weak_ptr的过程中,被管理的对象已经被其他线程被释放的情况。如果从weak_ptr构造shared_ptr仍然是通过add_ref_copy函数完成的,则可能发生以下错误情况:


 

线程1,从weak_ptr创建shared_ptr

线程2,释放目前唯一存在的shared_ptr

1

判断use_count_大于0,等待执行add_ref_copy

 

2

 

调用release,use_count--。发现use_count为0,删除被管理的对象

3

开始执行add_ref_copy,导致 use_count递增。

发生错误,use_count==1,但是对象已经被删除了

 


我们自然会想,线程1在第三行结束后,再判断一次use_count是否为1,如果是1,认为对象已经删除,判断失败不就可以了吗。其实是行不通的,下面是一个反例。

 

线程1,从weak_ptr创建shared_ptr

线程2,释放目前唯一存在的shared_ptr

线程3,从weak_ptr创建shared_ptr

1

判断use_count_大于0,等待执行add_ref_copy

 

 

2

 

 

判断use_count_大于0,等待执行add_ref_copy

3

 

调用release,use_count--。发现use_count为0,删除被管理的对象

 

4

开始执行add_ref_copy,导致 use_count递增。

 

 

5

 

 

执行add_ref_copy,导致 use_count递增。

6

发现use_count_ != 1,判断执行成功。

发生错误,use_count==2,但是对象已经被删除了

 

发现use_count_ != 1,判断执行成功。

发生错误,use_count==2,但是对象已经被删除了


实际上,boost从weak_ptr构造shared_ptr不是调用add_ref_copy,而是调用add_ref_lock函数。add_ref_lock是典型的无锁修改共享变量的代码,下面再把它的代码复制一遍,并添加证明注释。


    bool add_ref_lock(){

        for( ;; )

        {

            // 第一步,记录下use_count_

            long tmp = static_cast< long const volatile& >( use_count_ );

            // 第二步,如果已经被别的线程抢先清0了,则被管理的对象已经或者将要被释放,返回false

            if( tmp == 0 ) return false;

            // 第三步,如果if条件执行成功,

         // 说明在修改use_count_之前,use_count仍然是tmp,大于0

            // 也就是说use_count_在第一步和第三步之间,从来没有变为0过。

            // 这是因为use_count一旦变为0,就不可能再次累加为大于0

            // 因此,第一步和第三步之间,被管理的对象不可能被释放,返回true。

            if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;

        }

    }


在上面的注释中,用到了一个没有被证明的结论,“use_count一旦变为0,就不可能再次累加为大于0”。下面四条可以证明它。

1.use_count_是sp_counted_base类的private对象,sp_counted_base也没有友元函数,因此use_count_不会被对象外的代码修改。

2.成员函数add_ref_copy可以递增use_count_,但是所有对add_ref_copy函数的调用都是通过一个shared_ptr对象执行的。既然存在shared_ptr对象,use_count在递增之前一定不是0。

3.成员函数add_ref_lock可以递增use_count_,但正如add_ref_lock代码所示,执行第三步的时候,tmp都是大于0的,因此add_ref_lock不会使use_count_从0递增到1

4.其它成员函数从来不会递增use_count_

至此,我们可以放下心来,只要add_ref_lock返回true,递增引用计数的行为就是成功的。因此从weak_ptr构造shared_ptr的行为也是完全确定的,要么add_ref_lock返回true,构造成功,要么add_ref_lock返回false,构造失败。

综上所述,多线程通过不同的shared_ptr或者weak_ptr对象并发修改同一个引用计数对象sp_counted_base是线程安全的。而sp_counted_base对象是这些智能指针唯一操作的共享内存区,因此最终的结果就是线程安全的。

其它操作
前面我们分析了,不同的shared_ptr对象可以被多线程同时修改。那其它的问题呢,同一个shared_ptr对象可以对多线程同时修改吗?我们必须要注意到,前面所有的同步都是针对引用计数类sp_counted_base进行的,shared_ptr本身并没有任何同步保护。我们看下面boost文档举出来的非线程安全的例子


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

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


下面是shared_ptr类相关的代码

template<class Y>

void reset(Y * p)

{
     this_type(p).swap(*this);
}

void swap(shared_ptr<T> & other)

{
     std::swap(px, other.px);
     pn.swap(other.pn);
}


可以看到,reset执行了两个修改成员变量的操作,thread A和thread B的执行结果可能是非法的。。

但是仿照内置对象的语义,boost提供了若干个原子函数,支持通过这些函数并发修改同一个shared_ptr对象。这包括atomic_store、atomic_exchange、atomic_compare_exchange等。以下是实现的代码,不再详细分析。


template<class T>
void atomic_store( shared_ptr<T> * p, shared_ptr<T> r ){
    boost::detail::spinlock_pool<2>::scoped_lock lock( p );
    p->swap( r );
}

template<class T>
shared_ptr<T> atomic_exchange( shared_ptr<T> * p, shared_ptr<T> r ){
    boost::detail::spinlock & sp = boost::detail::spinlock_pool<2>::spinlock_for( p );

    sp.lock();
    p->swap( r );
    sp.unlock();

    return r;
}

template<class T>
bool atomic_compare_exchange( shared_ptr<T> * p, shared_ptr<T> * v, shared_ptr<T> w ){

    boost::detail::spinlock & sp = boost::detail::spinlock_pool<2>::spinlock_for( p );
    sp.lock();
    if( p->_internal_equiv( *v ) ){
        p->swap( w );
        sp.unlock();
        return true;
    }
    else{
        shared_ptr<T> tmp( *p );
        sp.unlock();
        tmp.swap( *v );
        return false;
    }
}

总结
正如boost文档所宣称的,boost为shared_ptr提供了与内置类型同级别的线程安全性。这包括:

1. 同一个shared_ptr对象可以被多线程同时读取。

2. 不同的shared_ptr对象可以被多线程同时修改。

3. 同一个shared_ptr对象不能被多线程直接修改,但可以通过原子函数完成。

如果把上面的表述中的"shared_ptr"替换为“内置类型”也完全成立。

最后,整理这个东西的时候我也发现有些关键点很难表述清楚,这也是由于线程安全性本身比较难严格证明。如果想要完全理解,还是建议阅读shared_ptr完整的代码。

--结束END--

本文标题: shared_ptr线程安全性全面分析

本文链接: https://www.lsjlt.com/news/172736.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • shared_ptr线程安全性全面分析
    正如《STL源码剖析》所讲,“源码之前,了无秘密”。本文基于shared_ptr的源代码,提取了shared_ptr的类图和对象图,然后分析了shared_ptr如何保证文档所宣称的...
    99+
    2022-11-15
    shared_ptr 线程安全性
  • Java线程安全与不安全实例分析
    本篇内容主要讲解“Java线程安全与不安全实例分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java线程安全与不安全实例分析”吧!当我们查看JDK API的时候,总会发现一些类说明写着,线程...
    99+
    2023-06-17
  • Python线程安全实例分析
    这篇文章主要介绍“Python线程安全实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python线程安全实例分析”文章能帮助大家解决问题。一、什么是线程安全?线程安全,名字就非常直接,在多线...
    99+
    2023-06-29
  • Java线程安全与非线程安全解析
    ArrayList和Vector有什么区别?HashMap和HashTable有什么区别?StringBuilder和StringBuffer有什么区别?这些都是Java面试中常见的基础问题。面对这样的问题,回答是:ArrayList是非线...
    99+
    2023-05-31
    java 线程安全 ava
  • Java线程之安全与不安全的示例分析
    这篇文章将为大家详细讲解有关Java线程之安全与不安全的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。当我们查看JDK API的时候,总会发现一些类说明写着,线程安全或者线程不安全,比如说Stri...
    99+
    2023-05-30
    java
  • python线程安全的示例分析
    这篇文章给大家分享的是有关python线程安全的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。python可以做什么Python是一种编程语言,内置了许多有效的工具,Python几乎无所不能,该语言通俗易...
    99+
    2023-06-14
  • 基于java线程安全问题及原理性分析
    1、什么是线程安全问题?从某个线程开始访问到访问结束的整个过程,如果有一个访问对象被其他线程修改,那么对于当前线程而言就发生了线程安全问题;如果在整个访问过程中,无一对象被其他线程修改,就是线程安全的。2、线程安全问题产生的根本原因首先是多...
    99+
    2023-05-31
    java 线程安全 ava
  • Java线程安全中的有序性浅析
    什么是有序性 在开发中,我们通常按照从上到下的顺序编写程序指令,并且希望cpu和编译器按照我们预先编写的顺序去执。但往往cpu和编译器为了提高性能、优化指令的执行顺序,会将我们编写好...
    99+
    2023-02-21
    Java线程有序性 Java线程安全 Java线程安全有序性
  • Java线程安全中的原子性浅析
    目录何为原子性解决方法CAS机制(Compare And Swap)何为原子性 原子性:一条线程在执行一系列程序指令操作时,该线程不可中断。一旦出现中断,那么就可能会导致程序执行前后...
    99+
    2023-02-21
    Java线程原子性 Java线程安全原子性
  • JAVA多线程线程安全性基础
    目录线程安全性什么是线程安全的代码什么是线程安全性 总结线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能 什么是线程安全的代码 核心:...
    99+
    2024-04-02
  • java中stringbuffer线程安全分析的示例
    这篇文章主要介绍java中stringbuffer线程安全分析的示例,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!常用的java框架有哪些1.SpringMVC,Spring Web MVC是一种基于Java的实现了...
    99+
    2023-06-14
  • java中线程安全问题举例分析
    这篇文章主要讲解了“java中线程安全问题举例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“java中线程安全问题举例分析”吧!一、什么时候数据在多线程并发的环境下会存在安全问题?三个条...
    99+
    2023-06-21
  • Java线程安全与同步实例分析
    本篇内容介绍了“Java线程安全与同步实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!线程安全问题多个线程可能会共享(访问)同一个资源...
    99+
    2023-06-30
  • java线程安全锁ReentrantReadWriteLock原理分析readLock
    目录前言ReentrantReadWriteLock的简单使用readLock源码分析lock()acquireShared()tryAcquireShared()fullTryAc...
    99+
    2024-04-02
  • Java线程安全状态的示例分析
    这篇文章主要为大家展示了“Java线程安全状态的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java线程安全状态的示例分析”这篇文章吧。一、观察线程的所有状态线程的状态是一个枚举类型 ...
    99+
    2023-06-29
  • Java多线程中线程安全问题的示例分析
    这篇文章主要介绍了Java多线程中线程安全问题的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1. 什么是线程安全和线程不安全?什么是线程安全呢?当多个线程并发访问某...
    99+
    2023-06-29
  • Java线程中的安全策略实例分析
    这篇文章主要介绍“Java线程中的安全策略实例分析”,在日常操作中,相信很多人在Java线程中的安全策略实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java线程中的安全策略实例分析”的疑惑有所帮助!...
    99+
    2023-06-30
  • 分析Web应用安全性HTTP
    本篇内容介绍了“分析Web应用安全性HTTP”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!概述如前所述,H...
    99+
    2024-04-02
  • PHP Session 跨域安全性分析
    概述:PHP Session 是一种在 Web 开发中常用的技术,用于跟踪用户的状态信息。虽然 PHP Session 在一定程度上提高了用户体验,但它也存在一些安全性问题,其中之一就是跨域安全性问题。本文将对 PHP Session 的跨...
    99+
    2023-10-21
    PHP 编程 安全性分析 PHP Session 跨域
  • 分析线程和线程安全的5个步骤分别是什么
    本篇文章为大家展示了分析线程和线程安全的5个步骤分别是什么,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。 什么是线程中断?在我们的Java程序中其实有不止一条执行线程,只有当所有的线程都运...
    99+
    2023-06-17
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作