面试题:C++中shared_ptr是线程安全的吗?——作业帮面试题

旅行   2024-11-08 08:08   广东  

欢迎关注本公众号,专注面试题拆解

分享一套视频课程:《C++实现百万并发服务器》 面试需要项目的可以找我获取,免费分享。 欢迎V:fb964919126


面试题:C++中shared_ptr是线程安全的吗?


std::shared_ptr 是 C++ 标准库中的智能指针,用于管理动态分配的对象,并自动释放不再需要的对象,它在 C++ 中是部分线程安全的,但这并不意味着它在所有情况下都是安全的。


01

线程安全的部分


引用计数操作是线程安全的

std::shared_ptr 的引用计数(即管理对象的共享所有权的计数)是线程安全的。因为std::shared_ptr 的内部引用计数是原子的这意味着多个线程可以安全地对同一个 std::shared_ptr 对象进行引用计数的操作(如 shared_ptr 的拷贝构造和赋值)。这些操作不会导致数据竞争。

我们看个例子

std::shared_ptr<int> p = std::make_shared<int>(0);constexpr int N = 10000;std::vector<std::shared_ptr<int>> sp_arr1(N);std::vector<std::shared_ptr<int>> sp_arr2(N);
void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) { for (int i = 0; i < N; i++) { sp_arr[i] = p; }}int main(){ std::thread t1(increment_count, std::ref(sp_arr1)); std::thread t2(increment_count, std::ref(sp_arr2)); t1.join(); t2.join(); std::cout << p.use_count() << std::endl; // always 20001
return 0;}

初始引用计数:

p 初始时有一个引用计数,因为它本身就是一个 std::shared_ptr。因此,初始的引用计数是 1。

线程 t1 和 t2 的操作:

每个线程将 p 赋值给一个包含 10000 个元素的向量中的每个元素。

每次赋值操作都会增加 p 的引用计数。

由于有两个线程,每个线程都会增加 10000 次引用计数。

因此,总的引用计数增加量是 10000+10000=20000

最终引用计数:

初始引用计数 1 加上两个线程增加的引用计数 20000,总引用计数为 1+20000=20001


这里的关键在于每次赋值操作都会原子地增加引用计数。因此,即使两个线程同时执行 sp_arr[i] = p;,也不会导致数据竞争或未定义行为


02

线程不安全的部分


2.1、 对象的访问


尽管 std::shared_ptr 的引用计数是线程安全的,但对所管理对象的访问并不是线程安全的。如果多个线程同时访问同一个 shared_ptr 管理的对象,并且至少有一个线程在修改该对象,那么就需要额外的同步机制(如互斥锁)来确保线程安全。

std::shared_ptr<int> p1 = std::make_shared<int>(0);void modify_memory() {    for (int i = 0; i < 10000; i++) {        (*p1)++;    }}int main(){    std::thread t1(modify_memory);    std::thread t2(modify_memory);    t1.join();    t2.join();    std::cout << "Final value of p: " << *p1 << std::endl
return 0;}

上面的代码运行,输出的结果不是预想的20000,每次运行输出的结果都会发生变化。因此同时修改shared_ptr指向的对象不是线程安全的。


2.2、直接修改shared_ptr对象本身的指向


如果多个线程同时修改同一个 std::shared_ptr 对象的指向(例如,使用赋值操作或重置操作),这将导致数据竞争。

数据竞争可能导致以下问题:

引用计数的损坏:如果一个线程在修改 shared_ptr 的指向时,另一个线程也在修改它,可能会导致引用计数不一致,从而导致内存泄漏或双重释放。

未定义行为:访问已释放的内存或访问无效的指针。

#include <iostream>#include <memory>#include <thread>
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42); // 创建一个 shared_ptr
void modifySharedPtr() { // 直接修改 shared_ptr 的指向 sharedPtr = std::make_shared<int>(100); // 不安全的操作}
int main() { std::thread t1(modifySharedPtr); std::thread t2(modifySharedPtr);
t1.join(); t2.join();
std::cout << "Value: " << *sharedPtr << std::endl; // 可能导致未定义行为
return 0;}

多次运行上面的代码会发现,输出的value值有时候会是一个乱码数字,不是预期的100。为了避免这些问题,需要使用互斥锁(std::mutex)来同步对 sharedPtr 的修改。

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);  // 创建一个 shared_ptrstd::mutex mtx;  // 互斥锁
void modifySharedPtr() { std::lock_guard<std::mutex> lock(mtx); // 加锁 sharedPtr = std::make_shared<int>(100); // 安全地修改指向}

03

总结

1.std::shared_ptr 的引用计数操作是线程安全的。

2.对std::shared_ptr 所指向的对象的访问需要额外的同步机制(如 std::mutex)来保证线程安全。

3.直接修改 std::shared_ptr 对象本身的指向在多线程环境中是不安全的,可能导致数据竞争和未定义行为。

end



CppPlayer 



关注,回复【电子书】珍藏CPP电子书资料赠送

精彩文章合集

专题推荐

【专辑】计算机网络真题拆解
【专辑】大厂最新真题
【专辑】C/C++面试真题拆解

CppPlayer
一个专注面试题拆解的公众号
 最新文章