先说结论,智能指针都是非线程安全的。
多线程调度智能指针
这里案例使用的是shared_ptr
,其他的unique_ptr
或者weak_ptr
的结果都是类似的,如下多线程调度代码:
#include <memory>
#include <thread>
#include <vector>
#include <atomic>
#include <mutex>
#include <unistd.h>
#include <iostream>
#include <iostream>
using namespace std;
class PTR{
public:
PTR(){}
int GetData() {return data_;}
void SetData(int a) {
data_.store(a, std::memory_order_relaxed);
}
void DoSomething() {
for (int i = 0; i< 10; i++) {
data_.fetch_add(1, std::memory_order_relaxed);
}
}
private:
std::atomic<int> data_;
};
std::shared_ptr<PTR> ptr;
std::mutex mu;
void ThreadFunc(int num) {
if (!ptr) {
ptr.reset(new PTR());
ptr->SetData(2);
}
ptr->DoSomething();
std::cout << "thread " << num << " GetData " << ptr->GetData()
<< " ref_count: " << ptr.use_count() << std::endl;
ptr.reset();
}
int main(int args, char* argv[]) {
int threads = atoi(argv[1]);
std::vector<std::thread> thread_vec;
for (int i = 0; i < threads; i++) {
thread_vec.emplace_back(std::thread(ThreadFunc, i));
}
for (auto& a : thread_vec) {
a.detach();
}
return 0;
}
大体逻辑是多个线程访问shared_ptr<PTR> ptr
,每次执行之前如果发现这个ptr是空的,则会先初始化一下,再做一些累加逻辑,处理完成之后再设置为空。
因为ThreadFunc
中没有同步机制,我们多线程下可能的执行行为如下:
其中t1 < t2 < t3 <t4,也就是有可能thread1 在t3时刻 reset了一个空的ptr, 但是在t4时刻,thread2根据自己之前t2时判断的ptr不为空的逻辑,直接使用了空的ptr去访问数据,从而造成BAD_ACCESS问题。
因为我们在ThreadFunc
内修改 一个被全局共享的ptr,所以,这个时候我们可能想要知道shared_ptr在被修改的时候内部行为是什么样子的。
shared_ptr 的实现
其他的智能指针通过reset构造对象的逻辑大体相似。
shared_ptr实现其实很简单,这里主要还是看一下它的reset逻辑,即 使用一个新的对象初始化当前shared_ptr 引用的对象
_SafeConv<_Yp>
reset(_Yp* __p) // _Yp must be complete.
{
// Catch self-reset errors.
__glibcxx_assert(__p == 0 || __p != _M_ptr);
// 将__p 构造为shared_ptr,并且与当前shared_ptr进行地址以及引用计数的交换
__shared_ptr(__p).swap(*this);
}
// 直接交换两个对象的地址,再交换shared_ptr的引用计数
void
swap(__shared_ptr<_Tp, _Lp>& __other) noexcept
{
std::swap(_M_ptr, __other._M_ptr);
_M_refcount._M_swap(__other._M_refcount);
}
void
_M_swap(__shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
__r._M_pi = _M_pi;
_M_pi = __tmp;
}
所以,这个过程本身就不是原子的,再加上外部同一个线程函数内部多次修改全局shared_ptr的地址,线程安全问题显而易见。
而当我们通过operator->()
去访问shared_ptr 的时候则是没有任何问题的,多线程下只要不修改,任何的读都是ok的。
element_type*
operator->() const noexcept
{
_GLIBCXX_DEBUG_PEDASSERT(_M_get() != nullptr);
return _M_get();
}
element_type*
_M_get() const noexcept
{ return static_cast<const __shared_ptr<_Tp, _Lp>*>(this)->get(); }
};
/// Return the stored pointer.
element_type*
get() const noexcept
{ return _M_ptr; }
综上,大家在多线程内部使用共享的智能指针的时候需要减少对智能指针的修改,或者修改的时候加上锁同步,防止出现智能指针内部的不同步行为,对于ThreadFunc来说,修改以及访问的逻辑需要有锁的介入才行:
void ThreadFunc(int num) {
mu.lock();
if (!ptr) {
ptr.reset(new PTR());
ptr->SetData(2);
}
ptr->DoSomething();
std::cout << "thread " << num << " GetData " << ptr->GetData()
<< " ref_count: " << ptr.use_count() << std::endl;
ptr.reset();
mu.unlock();
}