跟我学C++中级篇—std::shared_ptr的线程安全性分析

一、变量的线程安全性

开发者经常遇到的一个问题就是,在多线程操作资源特别是变量时,会不会导致变量的失控。只要学习过多线程开发的,往往一开始就是写同个线程来同时操作一个变量,然后打印这个变量,会发现,这个变量时而变化几次,时而不会变化,让初学者感到非常困惑。

其实在后来掌握了多线程开发后,对这种现象也就明白了。但明白了,不代表换个马甲还明白,这就是很多开发者面临的问题。比如智能指针中的std::shared_ptr是不是线程安全的?为什么?能不能阐述一下。然后,估计不少同学就卡売了。

二、std::shared_ptr的线程安全性

如果非要给一个定论,std::shared_ptr并不是线程安全的。但是为什么很多开发者经常看到或听到说"std::shared_ptr是线程安全的"这个结论呢?其实有很多是无意的情况下,把一个std::shared_ptr的个别内容给简化说明然后以讹传讹了。

std::shared_ptr可以从三个角度来看它的线程安全性:

  1. std::shared_ptr的引用计数器
    在std::shared_ptr中,引用计数器是原子操作,其当然是线程安全的。这也是为什么说"std::shared_ptr是线程安全的"一个重要的来原
  2. std::shared_ptr的赋值
    如果需要给一个std::shared_ptr指针赋值或者它们之间互相赋值,std::shared_ptr就不是线程安全的了。此时需要使用同步机制进行控制
  3. 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作为应用非常广泛的智能指针,用起来确实方便,但其中也隐藏着一些细节上的雷区。特别在多线程应用中,一定要搞清楚其线程安全性的范围,不能想当然的进行相关的操作。要在应用中区分对指针本身还是对资源的操作。而在对指针本身的操作中,又要分清是对引用计数器操作还是对象本身的操作。只有在明白这些区别后,才能有针对性的采用的应对的方法。与诸君共勉。

相关推荐
xyq20241 小时前
jQuery Mobile 按钮:深度解析与最佳实践
开发语言
bbq粉刷匠2 小时前
Java--多线程--线程安全3
java·开发语言
2401_831920742 小时前
C++中的桥接模式
开发语言·c++·算法
Promising_GEO2 小时前
探索Python融合地学:绘制栅格数据经纬度剖面图
开发语言·python·遥感·地理
m0_743470372 小时前
C++中的桥接模式变体
开发语言·c++·算法
IT猿手2 小时前
MATLAB画四旋翼无人机,机翼可独立旋转
开发语言·matlab·无人机
96772 小时前
java数据类型解析以及相关八股文的题 String 到底是基本类型还是引用类型?
java·开发语言·python
会编程的土豆2 小时前
【影院管理系统】
开发语言
gulinigar2 小时前
C++中的观察者模式实战
开发语言·c++·算法