shared_ptr是线程安全的吗?

shared_ptr是线程安全的吗?

C++11中的shared_ptr是线程安全的吗?先说结论,部分是,部分不是。

shared_ptr自身线程安全性

我们首先看一下shared_ptr实现的原理,一个简单的shared_ptr实现如下:

//shared_point.h
#include <atomic>
template<class T>
class shared_point{
public:
    shared_point(T* p):ptr_(p),cnt_(new std::atomic<int>{1}){}
    shared_point(const shared_point<T>& rhs)
        :ptr_(rhs.ptr_),cnt_(rhs.cnt_)
    {
        ++(*cnt_);
    }
    shared_point(shared_point<T>&&) = delete;
    shared_point<T>& operator=(const shared_point<T>& rhs)
    {
        if(this == &rhs) return *this;
        if(--*cnt_ <= 0)
        {
            delete ptr_;
            delete cnt_;
        }
        ptr_ = rhs.ptr_;
        cnt_ = rhs.cnt_;
        ++(*cnt_);
    }
    shared_point<T>&& operator=(shared_point<T>&&) = delete; 
    ~shared_point(){
        if(--*cnt_ <= 0)
        {
            delete ptr_;
            delete cnt_;
        }
    }

    T operator*(){
        return *ptr_;
    }

    T* operator->(){
        return ptr_;
    }

    int use_count(){
        return *cnt_;
    }
private:
    T* ptr_;
    std::atomic<int>* cnt_;
};

当使用pass-by-value的方式传递shard_ptr对象时,由于引用计数是原子类型,所有++--操作都是线程安全的,不存在由于多线程导致的引用计数问题,释放时同理。

而拷贝赋值同样是线程安全的,因为资源的释放由引用计数决定,而shared_ptr对象本身没有和其他对象共享数据(ptr_cnt_都是独享,只是指向的对象是共享的),所以也是线程安全的。

所以使用传值方式传递shared_ptr变量,shared_ptr是线程安全的。

验证如下:

//shared_ptr.cpp
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#include <vector>
#include "shared_point.h"

struct Demo
{
    int i;
    long j;
};

void func(shared_point<Demo> ptr)
{
    shared_point<Demo> demo(new Demo{0,0});
    demo = ptr;
    // std::cout <<"use_count:" << ptr.use_count() << std::endl;
}

int main(int argc, char const *argv[])
{
    shared_point<Demo> demo(new Demo{42,1024});
    std::vector<std::thread> threads;
    threads.reserve(1024*16);
    for (size_t i = 0; i < 1024*16; i++)
    {
        threads.emplace_back([demo](){
            func(demo);
        });
    }
    // std::this_thread::sleep_for(std::chrono::seconds(1));
    // std::cout << demo.use_count() << std::endl;
    for(auto& t:threads){
        t.join();
    }
    std::cout << demo.use_count() << std::endl;
    return 0;
}
//g++ -std=c++11 shared_ptr.cpp -o test -lpthread
//.test
//1

main函数中启用了1024*16个线程,多次执行测试程序,结果一定为1

当使用pass-by-reference方式传递shared_ptr时,要注意引用的生命周期不能长于被引用方,否则,当被引用方的计数为0,资源被释放后,引用方对于引用对象的任何操作都是未定义的。

同时拷贝赋值同样是非线程安全的,原因在于,ptr_cnt_都是共享的,而拷贝赋值并非原子操作,所以可能存在数据竞争。

考虑以下情况:

if(--*cnt_ <= 0)
{
    delete ptr_;
    delete cnt_;
}
ptr_ = rhs.ptr_;
cnt_ = rhs.cnt_;

--*cnt_0时,自身的ptr_cnt_都被释放,此时当前线程的CPU时间片到期,切换到其他线程执行。而这个线程正好要访问这个shared_ptr,问题就产生了。

为什么pass-by-value不会出现以上情况呢?因为如果是值传递,如果有另一个线程的shared_ptr拥有相同的资源,那么引用计数一定大于等于2,而当前线程的引用计数减1一定大于0!

所以使用引用方式传递的shared_ptr不是线程安全的。

//shared_ptr2.cpp
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#include <vector>
#include "shared_point.h"

struct Demo
{
    int i;
    long j;
};

void func1(shared_point<Demo> ptr)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout <<"use_count 1:" << ptr.use_count() << std::endl;
}

void func2(shared_point<Demo>& ptr)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout <<"use_count 2:" << ptr.use_count() << std::endl;
}

std::vector<std::thread> threads(2);
void func(){
    shared_point<Demo> demo(new Demo{42,1024});
    
    threads[0] = std::thread([demo](){
        func1(demo);
    });
    threads[1] = std::thread([&demo](){
        func2(demo);
    });
}

int main(int argc, char const *argv[])
{
    func();
    threads[0].join();
    threads[1].join();
    return 0;
}
//g++ -std=c++11 shared_ptr2.cpp -o test -lpthread
//./test
//use_count 1:2
//use_count 2:-1508628736 未定义的

对象T的线程安全性

shared_ptr所管理的对象的线程安全性不是确定的。这取决于T本身的线程安全性。

shared_ptr<std::atomic<int>>的对象,多线程对其fetch_add是安全的,而对象的类型如果是shared_ptr<std::vector<int>>,多线程对其push_back则是不安全的。