这个话题真的很老了,不过今天的实验还是没有得出结论,所以就简单的记录一下实验过程吧,希望能够抛砖引玉,静待评论区的大神更深入的分析。
本文在实验过程中,主要参考了c++ 11 的shared_ptr多线程安全?里,果冻虾仁的回复。
我们先限制一下这里讨论的线程安全问题的范围:只考虑 shared_ptr 所管理的指针本身的创建和销毁过程是否线程安全,即 shared_ptr 所管理的自增和自减部分是否是安全的。至于指针所指向的类的自身构造和析构过程的线程安全,不属于讨论范围。
先 PO 完整的测试代码,再拆解来分析
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include 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 m_data;Ref(initializer_list l): m_data(l) {cout << "Ref(initializer_list 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 thread_func(shared_ptr& p) { // demo according to https://www.zhihu.com/question/56836057cout << "ready" << endl;shared_lock lock(gMutex); // comment this line out, see anything happenscout << "running" << endl;shared_ptr n{nullptr};n = p;p = nullptr;p = n;return n;
}int main()
{int nNumbers = 1000;int nThreads = 10000;int secsDelay = 5;list threads;{shared_ptr creator{new Ref{1,2,3}};for (int i = 0; i < nNumbers; ++i) {creator->m_data.push_back(i);}unique_lock 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 gMutex;shared_ptr thread_func(shared_ptr& p) { // demo according to https://www.zhihu.com/question/56836057cout << "ready" << endl;// 关键行,让所有线程都在此处停留,用“读锁”保证“写锁”释放前,不会执行指针操作。// 同时保证,写锁释放后,所有线程能够并行的从这里开始执行(起跑)shared_lock lock(gMutex);cout << "running" << endl;shared_ptr n{nullptr};n = p;p = nullptr;p = n;return n;
}int main()
{int nNumbers = 1000;int nThreads = 10000;int secsDelay = 5;list threads;{shared_ptr creator{new Ref{1,2,3}};for (int i = 0; i < nNumbers; ++i) {creator->m_data.push_back(i);}// 用“写锁”,确保所有线程(包含“写锁”)不会执行,类似于画了一根“起跑线”unique_lock 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 thread_func(shared_ptr p) { // @@@ 传指针的值cout << "ready" << endl;shared_lock lock(gMutex);cout << "running" << endl;shared_ptr n{p};// @@@ 删除原来的赋值操作return n;
}int main()
{int nNumbers = 1000;int nThreads = 10000;int secsDelay = 5;list threads;{shared_ptr creator{new Ref{1,2,3}};for (int i = 0; i < nNumbers; ++i) {creator->m_data.push_back(i);}unique_lock 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;
}