这个话题真的很老了,不过今天的实验还是没有得出结论,所以就简单的记录一下实验过程吧,希望能够抛砖引玉,静待评论区的大神更深入的分析。
本文在实验过程中,主要参考了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;
}