一、变量的线程安全性
开发者经常遇到的一个问题就是,在多线程操作资源特别是变量时,会不会导致变量的失控。只要学习过多线程开发的,往往一开始就是写同个线程来同时操作一个变量,然后打印这个变量,会发现,这个变量时而变化几次,时而不会变化,让初学者感到非常困惑。
其实在后来掌握了多线程开发后,对这种现象也就明白了。但明白了,不代表换个马甲还明白,这就是很多开发者面临的问题。比如智能指针中的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,最好的方法是使用C++20中的新标准std::atomic(std::shared_ptr)(在原来的实验库中有过一个atomic_shared_ptr)。但对于不少的开发者来说,可能这个标准有点高。否则的话,就只能回到传统的同步机制来保证shared_ptr的多线程环境下的安全性(在某些情况下可以借助一下std::weak_ptr,通过多写几行代码来判断)。
通过分析,就可以明白,在多线程中操作同一std::shared_ptr,要传递拷贝而非引用(引用计数器是安全的),除了这种情况下对std::shared_ptr的操作,一般都需要使用同步机制。
四、例程
下面给出一个简单的对比例程,让大家更容易问题的所在:
c
#include <memory>
#include <thread>
#include <iostream>
#include <mutex>
#include <vector>
std::shared_ptr<int> spInit = std::make_shared<int>(100);
std::mutex mtx;
// 安全 :多线程直接传递,操作引用计数器
void safeDemoUsed() {
std::shared_ptr<int> local_sp = spInit;
}
//不安全:智能指针对象本身重新赋值需要同步机制
void unsafeDemoReUsed() {
spInit = std::make_shared<int>(0);
}
//不安全:操作智能指针指向的对象需要同步
void unsafeDemoUsedData() {
//std::lock_guard<std::mutex> lock(mtx);
*spInit = 0;
}
// 安全 :C++20使用atomic操作
#include <atomic>
std::atomic<std::shared_ptr<int>> asp = std::make_shared<int>(121);
void safeDemoAtomic() {
auto oldSp = asp.load();
auto newSp = std::make_shared<int>(111);
asp.store(newSp);
}
int main(){
//可参考下面的代码调用其它函数即可
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(safeDemoUsed);
}
for (auto &th : threads) {
th.join();
}
return 0;
}
请自行完善线程相关处理的并发问题,上面代码只是一个原型
注意:C++20标准的使用
五、总结
std::shared_ptr作为应用非常广泛的智能指针,用起来确实方便,但其中也隐藏着一些细节上的雷区。特别在多线程应用中,一定要搞清楚其线程安全性的范围,不能想当然的进行相关的操作。要在应用中区分对指针本身还是对资源的操作。而在对指针本身的操作中,又要分清是对引用计数器操作还是对象本身的操作。只有在明白这些区别后,才能有针对性的采用的应对的方法。与诸君共勉。