shared_ptr 是线程安全的吗?

这个话题真的很老了,不过今天的实验还是没有得出结论,所以就简单的记录一下实验过程吧,希望能够抛砖引玉,静待评论区的大神更深入的分析。

本文在实验过程中,主要参考了c++ 11 的shared_ptr多线程安全?里,果冻虾仁的回复

我们先限制一下这里讨论的线程安全问题的范围只考虑 shared_ptr 所管理的指针本身的创建和销毁过程是否线程安全,即 shared_ptr 所管理的自增和自减部分是否是安全的。至于指针所指向的类的自身构造和析构过程的线程安全,不属于讨论范围

先 PO 完整的测试代码,再拆解来分析

#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <sstream>
#include <thread>
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <functional>

using namespace std;

void new_section(string section, string msg) {
    cout << endl;
    cout << "#################################################################" << endl;
    cout << "# [" << section << "] " << msg << endl;
    cout << "#################################################################" << endl;
}

class Ref {
public:
    std::vector<int> m_data;

    Ref(initializer_list<int> l): m_data(l) {
        cout << "Ref(initializer_list<int> l)" << endl;
    }

    string to_string() {
        stringstream ss;
        ss << "Ref(";
        bool first = true;
        for (auto v: m_data) {
            if (!first) { ss << ", "; }
            ss << v;
        }
        ss << ")";
        return ss.str();
    }
};

shared_mutex gMutex;

shared_ptr<Ref> thread_func(shared_ptr<Ref>& p) {  // demo according to https://www.zhihu.com/question/56836057
    cout << "ready" << endl;
    shared_lock<shared_mutex> lock(gMutex);  // comment this line out, see anything happens
    cout << "running" << endl;
    shared_ptr<Ref> n{nullptr};
    n = p;
    p = nullptr;
    p = n;
    return n;
}

int main()
{
    int nNumbers = 1000;
    int nThreads = 10000;
    int secsDelay = 5;
    list<thread> threads;
    {
        shared_ptr<Ref> creator{new Ref{1,2,3}};
        for (int i = 0; i < nNumbers; ++i) {
            creator->m_data.push_back(i);
        }
        unique_lock<shared_mutex> lock(gMutex);
        for (int i = 0; i < nThreads; ++i) {
            threads.push_back(thread(bind(thread_func, std::ref(creator))));
        }
        for (int i = 0; i < secsDelay; ++i) {
            this_thread::sleep_for(chrono::seconds(1));
            cout << "counting " << i << endl;
        }
    }
    for (auto& t: threads) {
        t.join();
    }

    return 0;
}

首先,我们使用互斥量 gMutex 和锁 unique_lock<shared_mutex> 来完成操作的同时开始,类似于一个“起跑线”,让所有线程的 "n = 0; p = nullptr; p = n;" 操作,能够同时进行。


shared_mutex gMutex;

shared_ptr<Ref> thread_func(shared_ptr<Ref>& p) {  // demo according to https://www.zhihu.com/question/56836057
    cout << "ready" << endl;
    // 关键行,让所有线程都在此处停留,用“读锁”保证“写锁”释放前,不会执行指针操作。
    //   同时保证,写锁释放后,所有线程能够并行的从这里开始执行(起跑)
    shared_lock<shared_mutex> lock(gMutex);
    cout << "running" << endl;
    shared_ptr<Ref> n{nullptr};
    n = p;
    p = nullptr;
    p = n;
    return n;
}

int main()
{
    int nNumbers = 1000;
    int nThreads = 10000;
    int secsDelay = 5;
    list<thread> threads;
    {
        shared_ptr<Ref> creator{new Ref{1,2,3}};
        for (int i = 0; i < nNumbers; ++i) {
            creator->m_data.push_back(i);
        }
        // 用“写锁”,确保所有线程(包含“写锁”)不会执行,类似于画了一根“起跑线”
        unique_lock<shared_mutex> lock(gMutex);
        for (int i = 0; i < nThreads; ++i) {
            // 关键行,启动多个线程
            threads.push_back(thread(bind(thread_func, std::ref(creator)))); 
        }
        for (int i = 0; i < secsDelay; ++i) {
            this_thread::sleep_for(chrono::seconds(1));
            cout << "counting " << i << endl;
        }
        // 利用锁的生命周期来解“写锁”,“写锁”接触后,所有线程即开始执行,类似于“起跑”
    }
    for (auto& t: threads) {
        t.join();
    }

    return 0;
}

本实验的测试结果,并未出现崩溃或者DEBUG报错。

但是从果冻虾仁的回复来看,最关键的,莫过于在 thread_func 里面,对入参 p 的两次赋值操作,即下面两行

    p = nullptr;
    p = n;

如果有异常情况,我的理解,应该是会有崩溃或者DEBUG的 Assert failuer 之类的错误。

所以从这个实验来看,应该还算是线程安全的。

当然,按照理论分析,问题确实看上去是有的,也可能是笔者的实验设定不能体现这个问题罢了。​​​​​​​

按照《c++ 11 的shared_ptr多线程安全?》里面其他回答来看,我们在跨线程的情况下,只要做到以下两点,应该就问题不大:

1. 传递指针的值

2. 不修改指针的值

那么如何修改这段代码呢?


shared_ptr<Ref> thread_func(shared_ptr<Ref> p) {  // @@@ 传指针的值
    cout << "ready" << endl;
    shared_lock<shared_mutex> lock(gMutex);
    cout << "running" << endl;
    shared_ptr<Ref> n{p};
    // @@@ 删除原来的赋值操作
    return n;
}

int main()
{
    int nNumbers = 1000;
    int nThreads = 10000;
    int secsDelay = 5;
    list<thread> threads;
    {
        shared_ptr<Ref> creator{new Ref{1,2,3}};
        for (int i = 0; i < nNumbers; ++i) {
            creator->m_data.push_back(i);
        }
        unique_lock<shared_mutex> lock(gMutex);
        for (int i = 0; i < nThreads; ++i) {
            // @@@ 绑定时,也绑定值,而不是引用
            threads.push_back(thread(bind(thread_func, creator))); 
        }
        for (int i = 0; i < secsDelay; ++i) {
            this_thread::sleep_for(chrono::seconds(1));
            cout << "counting " << i << endl;
        }
    }
    for (auto& t: threads) {
        t.join();
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值