shared_ptr:线程安全、循环引用

本文深入探讨了 C++ 中 shared_ptr 的使用方法及注意事项,包括线程安全问题、循环引用问题及其解决方案,同时还提供了多线程环境下正确使用 shared_ptr 的实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

shared_ptr:线程安全、循环引用

shared_ptr用来管理堆对象可以避免delete,但是注意shared_ptr本身是个对象因此其的声明周期、shared_ptr对象的读写操作非原子化那么在多线程环境下仍然存在很多问题,即shared_ptr对象本身的线程安全级别非100%。在多线程中访问同一个shared_ptr对象应该用mutex保护。
shared_ptr使用不当会延长锁管理的对象(假设为A)的生命周期,即有什么地方不小心留下一个shared_ptr的拷贝,那么对象A生命期无疑被延长了,因为对象计数不为0将不会被销毁。boost::bind会将函数参数拷贝一份,那么

shared_ptr<T>one, boost::function<void()> fp=boost::bind(&fun,one)//这种情况会下one管理的对象生命期一定晚于fp对象。

shared_ptr可能会导致循环引用即:对象A持有一个关于B的shared_ptr,B持有一个关于A的shared_ptr,那么A、B离开作用域的时候它们的计数都不为0(因为对方持有一个shared_ptr指向自己)而谁也不肯先释放这个shared_ptr,最后造成内存泄露。解决方法是:A持有关于B的shared_ptr,B持有关于A的weak_ptr。
当一个管理对象A的shared_ptr离开作用域且计数变为0时那么对象A将在这个线程析构(这个线程可能不是创建A的线程),若析构比较费事,那么析构A的线程将被拖累,解决办法是:找个专门的线程管理所有的shared_ptr对象,并执行析构操作。
我曾犯过的一个错误:若shared_ptr管理对象A,假设对象里面有new的堆内存,在对象A销毁前(即shared_ptr计数大于0)的时候,在某个地方A通过某些操作释放了A里里面的堆内存….那么最后的结果是?不错,double free,在A存活的时候释放了A里面的部分内存,那么在shared_ptr计数为0要销毁A的时候又去释放那块内存,然后就double free了。所以记住:避免shared_ptr管理的对象直接的内存管理操作以免造成重复释放。
下面是多线程共享一个shared_ptr对象时的保护措施:

#include<iostream>  
#include<string>  
#include<unistd.h>  
#include<pthread.h>  
#include<boost/shared_ptr.hpp>  
#include<boost/weak_ptr.hpp>  
using namespace std;  
using namespace boost;  
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//保护globalPtr的互斥量  
class test{//shared_ptr管理的对象(假设为A)  
    public:  
        void show(string s){  
            cout<<"test::show()  in: "<<s<<endl;  
        }  
};  
shared_ptr<test> globalPtr(new test);  

void fun(const shared_ptr<test>& ptr,string s){//用于展示多层函数传递shared_ptr时只用最外层函数持有shared_ptr对象实体,内层函数可以用const reference传递,毕竟大多数都是操作shared_ptr管理的对象而不是shared_ptr本身,const reference不会遇见返回拷贝shared_ptr对象导致的性能问题  
    ptr->show(s);//操作shared_ptr管理的对象  
}  
void read(){//读globalPtr时候使用mutex保护  
    shared_ptr<test> localptr;//减小临界区  
    {  
        pthread_mutex_lock(&mutex);  
        localptr=globalPtr;//以后的操作只与localptr有关  
        pthread_mutex_unlock(&mutex);  
    }  
    fun(localptr,"read()");  
}  
void write(){//重写globalPtr时使用mutex保护  
    shared_ptr<test> newptr(new test);//重写globalPtr  
    shared_ptr<test> lockptr;//###1  
    {  
        pthread_mutex_lock(&mutex);  
        //globalPtr=newptr;//###2//这里与###1的区别是:直接使用本语句将有可能出现:globalPtr管理的对象A计数为1即这里将newptr赋给globalPtr后对象A将会被析构,则会使临界区变长,那么解决方式就是按照###1的那几句话  
        lockptr.swap(globalPtr);//###1  
        globalPtr=newptr;//###1  
        pthread_mutex_unlock(&mutex);  
    }  
    fun(newptr,"write()");  
    //fun(shared_ptr<test> one(new test),g()//构造一个临时的shared_ptr作为函数参数,函数参数求值顺序不一定,可能:new test第一,g()第二但是如果g()抛出一个异常,那么永远不可能到达shared_ptr的构造函数  
}  
void reset(){//销毁shared_ptr对象  
    shared_ptr<test> localptr;//同样为了缩小临界区  
    {  
        pthread_mutex_lock(&mutex);  
        localptr.swap(globalPtr);//globalPtr轻松就置空了且不会引起所管理的对象A在这里被析构,临界区非常小  
        pthread_mutex_unlock(&mutex);  
    }  
    fun(localptr,"reset()");  
}  
void* worker(void* arg){  
    int* x=(int*)arg;  
    switch(*x){  
        case 1:  
            read();  
            break;  
        case 2:  
            write();  
            break;  
        case 3:  
            sleep(1);//pid3线程为了让其它两个线程执行完,不然这里使globalPtr置空了其它线程会报错  
            reset();  
            break;  
        default:  
            cout<<"are you kidding me"<<endl;  
    }  
}  
int main(){  
    pthread_t pid1,pid2,pid3;  
    int i=1;  
    pthread_create(&pid1,NULL,worker,&i);  
    int j=2;//本来想用i++的但是所有线程都执行i=3的情形这说明:在前面两个线程还没有提取i=1,i=2的时候,线程3已经将i置为3了那么前两个线程就提取了i=3,这个错误以前犯过...哎...  
    pthread_create(&pid2,NULL,worker,&j);  
    int k=3;  
    pthread_create(&pid3,NULL,&worker,&k);  
    pthread_join(pid1,NULL);  
    pthread_join(pid2,NULL);  
    pthread_join(pid3,NULL);  
    return 0;  
}  

输出结果:

test::show() in: write()test::show() in: read()
test::show() in: reset()

shared_ptr循环引用的例子:内存泄露问题

#include<iostream>  
#include<boost/shared_ptr.hpp>  
#include<boost/weak_ptr.hpp>  
using namespace std;  
using namespace boost;  
class B;  
class A{  
    public:  
        shared_ptr<B> ptr_A;  
        ~A(){  
            cout<<"~A()"<<endl;  
        }  
};  
class B{  
    public:  
        //shared_ptr<A> ptr_B;//当采用shared_ptr指向A时会形成循环引用,则什么都不会输出说明对象没有被析构,可怕的内存泄露....  
        weak_ptr<A> ptr_B;//当采用弱引用时,避免了循环引用,有输出,说明对象被析构了  
        ~B(){  
            cout<<"~B()"<<endl;  
        }  
};  
int main(){  
    shared_ptr<A> a(new A);  
    shared_ptr<B> b(new B);  
    a->ptr_A=b;  
    b->ptr_B=a;//若是循环引用:当a、b退出作用域的时候,A对象计数不为1(b保留了个计数呢),同理B的计数也不为1,那么对象将不会被销毁,内存泄露了...  
    return 0;  
}  

若是循环引用则什么输出都没有即对象没有析构,内存泄露了。
若是B持有A的weak_ptr则输出:

~A()
~B()

### C++ `std::unique_ptr` 与 `std::shared_ptr` 的区别及使用场景 #### 1. 概念差异 `std::unique_ptr` 和 `std::shared_ptr` 都是 C++ 标准库提供的智能指针类型,用于管理动态分配的对象。两者的根本区别在于所有权模型: - `std::unique_ptr` 实现了独占所有权语义,即同一时间只能有一个 `std::unique_ptr` 拥有某个对象的控制权[^2]。 - `std::shared_ptr` 实现了共享所有权语义,允许多个 `std::shared_ptr` 实例同时拥有同一个对象,并通过引用计数机制决定何时释放资源[^1]。 #### 2. 内部实现与内存占用 - `std::unique_ptr` 的内部结构相对简单,仅包含一个指向所管理对象的原始指针和一个删除器(默认为 `std::default_delete<T>`)。因此,它的内存占用通常较小,大小等于或接近于普通指针[^2]。 - `std::shared_ptr` 则需要额外维护一个控制块,其中包含引用计数、弱引用计数以及删除器等信息。这使得 `std::shared_ptr` 的内存开销较大,通常是 `std::unique_ptr` 的两倍或更多[^1]。 ```cpp #include <iostream> #include <memory> int main() { std::cout << "Size of unique_ptr: " << sizeof(std::unique_ptr<int>) << " bytes" << std::endl; // 通常为 8 字节 std::cout << "Size of shared_ptr: " << sizeof(std::shared_ptr<int>) << " bytes" << std::endl; // 通常为 16 字节 } ``` #### 3. 使用场景 - **`std::unique_ptr`**: - 当对象的所有权不需要被共享时,应优先使用 `std::unique_ptr`,因为它提供了更高的性能和更低的内存开销。 - 适用于那些只需要单个所有者并且在生命周期结束时自动释放资源的情况。 - **`std::shared_ptr`**: - 当多个对象或模块需要共享对同一资源的访问权限时,可以选择使用 `std::shared_ptr`。 - 注意避免循环引用问题,可以通过结合使用 `std::weak_ptr` 来解决[^3]。 #### 4. 示例代码 ##### `std::unique_ptr` 示例 ```cpp #include <iostream> #include <memory> struct Resource { void process() { std::cout << "Processing resource..." << std::endl; } ~Resource() { std::cout << "Resource destroyed!" << std::endl; } }; void use_unique_ptr() { std::unique_ptr<Resource> ptr = std::make_unique<Resource>(); ptr->process(); // ptr 在此函数结束时自动销毁 } ``` ##### `std::shared_ptr` 示例 ```cpp #include <iostream> #include <memory> struct SharedResource { void display() { std::cout << "Shared resource displayed." << std::endl; } ~SharedResource() { std::cout << "Shared resource destroyed!" << std::endl; } }; void use_shared_ptr() { std::shared_ptr<SharedResource> ptr1 = std::make_shared<SharedResource>(); ptr1->display(); { std::shared_ptr<SharedResource> ptr2 = ptr1; // 引用计数增加至 2 } // ptr2 超出作用域,引用计数减少至 1 // 当最后一个 shared_ptr 超出作用域时,资源被销毁 } ``` #### 5. 性能与安全性 - `std::unique_ptr` 提供零开销抽象,其操作效率几乎等同于裸指针。 - `std::shared_ptr` 因为涉及引用计数的维护,在多线程环境下可能带来额外的同步开销。然而,现代实现通常采用原子操作来优化这一过程[^1]。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值