跟我学C++中级篇—std::shared_ptr的线程安全性分析
一、变量的线程安全性开发者经常遇到的一个问题就是在多线程操作资源特别是变量时会不会导致变量的失控。只要学习过多线程开发的往往一开始就是写同个线程来同时操作一个变量然后打印这个变量会发现这个变量时而变化几次时而不会变化让初学者感到非常困惑。其实在后来掌握了多线程开发后对这种现象也就明白了。但明白了不代表换个马甲还明白这就是很多开发者面临的问题。比如智能指针中的std::shared_ptr是不是线程安全的为什么能不能阐述一下。然后估计不少同学就卡売了。二、std::shared_ptr的线程安全性如果非要给一个定论std::shared_ptr并不是线程安全的。但是为什么很多开发者经常看到或听到说“std::shared_ptr是线程安全的”这个结论呢其实有很多是无意的情况下把一个std::shared_ptr的个别内容给简化说明然后以讹传讹了。std::shared_ptr可以从三个角度来看它的线程安全性std::shared_ptr的引用计数器在std::shared_ptr中引用计数器是原子操作其当然是线程安全的。这也是为什么说“std::shared_ptr是线程安全的”一个重要的来原std::shared_ptr的赋值如果需要给一个std::shared_ptr指针赋值或者它们之间互相赋值std::shared_ptr就不是线程安全的了。此时需要使用同步机制进行控制std::shared_ptr操作的对象如果想在多线程中操作std::shared_ptr指向的对象如果这个对象本身没有锁或原子操作的话同样也需要线程间的同步机制来保证线程操作的安全通过上面的分析说明就可以从整体了明白std::shared_ptr在线程安全方面到底哪些是安全的。而不是简单的回答是与否。三、具体的分析std::shared_ptr主要解决的共享指针操作相同对象时引用数量的安全性。它重点是保护这个引用计数器是否是安全的。这才是其设计的主要目的在C标准中是这样描述的“The shared_ptr objects have the thread safety guarantees of a std::atomic for the control block, but not for the shared_ptr object itself.”。这也印证了刚刚的说明std::shared_ptr的设计目的是为解决多线程操作同一个std::shared_ptr对象而不能对这个线程对象改来改去。可以把std::shared_ptr的应用分成三块即处理多线程引用的计数器、std::shared_ptr对象本身和std::shared_ptr指向的数据块。它就可以映射到上面的三个角度看问题。一定要分清楚std::shared_ptr设计上只是满足了第一块。而对后面两部分并没有提供线程安全的机制即不保证其线程安全性。如果想安全的使用std::shared_ptr最好的方法是使用C20中的新标准std::atomic(std::shared_ptr)(在原来的实验库中有过一个atomic_shared_ptr)。但对于不少的开发者来说可能这个标准有点高。否则的话就只能回到传统的同步机制来保证shared_ptr的多线程环境下的安全性在某些情况下可以借助一下std::weak_ptr通过多写几行代码来判断。通过分析就可以明白在多线程中操作同一std::shared_ptr,要传递拷贝而非引用引用计数器是安全的除了这种情况下对std::shared_ptr的操作一般都需要使用同步机制。四、例程下面给出一个简单的对比例程让大家更容易问题的所在#includememory#includethread#includeiostream#includemutex#includevectorstd::shared_ptrintspInitstd::make_sharedint(100);std::mutex mtx;// 安全 多线程直接传递操作引用计数器voidsafeDemoUsed(){std::shared_ptrintlocal_spspInit;}//不安全智能指针对象本身重新赋值需要同步机制voidunsafeDemoReUsed(){spInitstd::make_sharedint(0);}//不安全操作智能指针指向的对象需要同步voidunsafeDemoUsedData(){//std::lock_guardstd::mutex lock(mtx);*spInit0;}// 安全 C20使用atomic操作#includeatomicstd::atomicstd::shared_ptrintaspstd::make_sharedint(121);voidsafeDemoAtomic(){autooldSpasp.load();autonewSpstd::make_sharedint(111);asp.store(newSp);}intmain(){//可参考下面的代码调用其它函数即可std::vectorstd::threadthreads;for(inti0;i3;i){threads.emplace_back(safeDemoUsed);}for(autoth:threads){th.join();}return0;}请自行完善线程相关处理的并发问题上面代码只是一个原型注意C20标准的使用五、总结std::shared_ptr作为应用非常广泛的智能指针用起来确实方便但其中也隐藏着一些细节上的雷区。特别在多线程应用中一定要搞清楚其线程安全性的范围不能想当然的进行相关的操作。要在应用中区分对指针本身还是对资源的操作。而在对指针本身的操作中又要分清是对引用计数器操作还是对象本身的操作。只有在明白这些区别后才能有针对性的采用的应对的方法。与诸君共勉。