欢迎光临散文网 会员登陆 & 注册

智能指针的源码原来这么好读(shared_ptr,weak_ptr)

2021-10-01 16:33 作者:NewtonCY  | 我要投稿

看了几篇别人写得文章,感觉写得模模糊糊的。。于是自己去读了读。

本文不仅精确而且完备。看了本文之后,即使自己实现一套智能指针也没有问题。

1、先定义数据

下文中

  • “智能指针”指shared_ptr或weak_ptr

  • “裸指针”指原始的C指针

  • 裸指针还是智能指针通常是上下文自明的,此时直接用“指针”一词指代两者之一

首先,我们来看shared_ptr和weak_ptr持有了哪些数据。

对于shared_ptr和weak_ptr,都仅持有两个字段,指向对象地址的指针ptr,和指向计数器对象的指针rep

对于计数器对象,我们仅需要关心两个字段,一个是弱引用计数weaks(无符号整型),一个是强引用计数uses(无符号整型)。

那么,若s1,s2是仅有的两个指向对象obj的shared_ptr,w1,w2,w3是仅有的三个指向对象obj的weak_ptr,则以上所有的智能指针(s1,s2,w1,w2,w3)的ptr字段保存着obj的地址,所有的rep字段指向同一个计数器对象(记为c1),c1的weaks字段现在是4(3+1,为什么加一后面会讲),uses字段现在是2。

此时,为了我写着方便,我们可以将s1和c1简记为   s1=(&obj,&c1);c1=(u:2,w:4)

指针中两个字段:ptr,rep。计数对象中两个字段:uses,weaks。请捋顺之后再向下阅读。

2.再定义算法

下面,我们来结合例子来讲每个操作背后发生了什么,注意:例子是连续的,即上一个操作的结束是下一个操作的开始。

操作1:构造新的空指针

shared_ptr<int> s1,s2;

weak_ptr<int> w1;

初始化操作十分简单,s1,s2和w1的ptr和rep都被初始为了NULL,

记作s1=s2=w1=(NULL,NULL)

操作2:使用裸指针构造智能指针

int* a=new int(1);

int* b= new int(2);

s1.reset(a);  

发生条件:用裸指针reset智能指针,或将裸指针作为参数调用构造函数

执行过程:

//创建一个新的计数器对象(记为c1),计数器对象的weaks和uses都初始为1

new c1=(u:1,w:1); 

// 将s1的ptr赋值为裸指针a,将s1的rep指向c1

s1=(a,&c1);  

操作3:从强引用构造智能指针

s2=s1;   // uses计数+1

w1=s1;  // weaks计数+1

weak_ptr<int> w2(w1); // weaks计数+1

发生条件:用shared_ptr构造智能指针或赋值空的智能指针。用weak_ptr构造weak_ptr或赋值空的weak_ptr。

执行过程:

// 若右值的rep非空,则复制右值的rep到左值,然后将rep的uses或weaks计数原子+1

if(s1.rep&&s1.rep->uses或weaks计数原子+1)

    s2.rep = s1.rep;

s2.ptr=s1.ptr;

结果:

c1=(u:2,w:3);s1=s2=w1=w2=(a,&c1)

以下结论始终成立:

uses计数=实际强引用数。

weaks计数=

1.实际弱引用数+1(当uses计数不为0)

2.实际弱引用数(当uses计数为0)

如果uses计数不为0,则weaks计数至少为1


操作4:从弱引用构造强引用

shared_ptr<int> s3(w1)

发生条件:用weak_ptr,构造或赋值空的shared_ptr。执行weak_ptr的lock函数

执行过程:

if(w1.rep){

    将w1.rep->uses字段加乐观锁{

        if(w1.rep->uses!=0){

            w1.rep->uses++;

            s3.rep=w1.rep;

            s3.ptr=w1.ptr;

        }else{

            抛出bad_weak_ptr异常

        }

    }

}

结果:

c1=(u:3,w:3);s1=s2=s3=w1=w2=(a,&c1)

操作5:释放弱引用

w1.reset()

发生条件:1.对weak_ptr做reset。2.见“一些组合操作”

执行过程:

w1.ptr=NULL;

w1.rep=NULL;

if(w1.rep->原子操作weaks-1并返回新的值 ==0){

    释放对象 w1.rep;

}

操作6:释放强引用

s1.reset()

发生条件:1.对shared_ptr做reset。2.见“一些组合操作”

执行过程:

s1.ptr=NULL

s1.rep=NULL

f(s1.rep->原子操作uses-1并返回新的值 ==0){

    对s1的rep额外执行一次释放弱引用操作;

    释放对象s1.ptr;

}

// 如果uses为0,weaks计数等于实际弱引用数,如果uses不为0,weaks计数等于实际弱引用数+1。因此如果users不为0则weaks至少为1。

操作7:一些组合操作和例子

此时,s2=s3=w2=(a,&c1); c1=(u:2,w:2)

操作:s2.reset(b);

相当于执行“强引用释放操作”: s2.reset(),然后再执行“从裸指针初始化”操作  s2.reset(b)

操作完成后,此时:

s3=w2=(a,&c1); c1=(u:1,w:2); s2=(b,&c2);c2=(u:1,w:1)


操作:s3=s2

相当于执行“强引用释放操作”: s3.reset(),然后执行“从强引用构造智能指针”操作:s3=s2

此时

w2=(a,&c1); c1=(u:0,w:1);s1=s2=(b,&c2);c2=(u:2,w:1)

注意:s3.reset()的过程中,因为c1的uses减为0。所以 delete a操作被执行,c1的weaks被额外-1。此时w2的ptr值依然等于指针a,但是delete a已经被执行,所以ptr=a是个野指针。但我们不会访问到这个野指针,因为“从弱引用构造强引用”这个操作会检查uses的计数,并抛出bad_weak_ptr异常。


操作:w2.reset()

这个操作是释放弱引用。举这个例子是想说,c1的weaks计数只剩下1了,此时执行这个操作将使c1的weaks计数减为0并释放c1。也标志着所有指向a的强/弱引用全部释放完毕。正如我反复强调的一样,如果uses不为0,即强引用不为0,则weaks计数至少为1。weaks为0则强弱引用的数量必然都为0。

另外,考虑一种情况。设此时有 w1=(a,&c1); c1=(u:0,w:1);w2=(NULL,NULL) 

如果一个线程p1执行w1.reset() 。同时线程p2执行w2=w1

这是不安全的。考虑下面这种执行顺序

p2: w2=w1   执行至  检查完成w1的rep非空,但是尚未执行引用计数的增加。

p1:w1.reset() 完全执行,此时weaks计数归0,w1将rep所指向的c1释放。

p2:w2=w1    继续执行。它将使用野指针w1.rep执行引用计数+1操作,并复制野指针rep和ptr。

当p1线程写w1时,p2读了w1。这是犯规的。智能指针的线程安全性仅限于:

  1. 多线程持有不同的智能指针对象,此时并发的操作这些对象是线程安全的。即使他们引用的计数器对象是同一个。

  2. 多线程并发的读同一个智能指针对象是安全的。

  3. 当一个线程正在修改一个智能指针对象时,其他线程对这个智能指针进行读和写都是不安全的。

主要操作的执行过程就讲完了。


3.分析它在并发下的正确性

写累了。。累死了。。。有人看再写

智能指针的源码原来这么好读(shared_ptr,weak_ptr)的评论 (共 条)

分享到微博请遵守国家法律