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

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

C++的智能指针有四种类型,去掉auto_ptr,主要有三种类型,定义在头文件中,用于管理动态分配的对象,并自动释放不再需要的对象,在C++中是部份线程安全的,但是并不意味着在在所有情况下都是安全的。

1.1 智能指针介绍

  1. std::unique_ptr

表示独占式拥有,不允许多个指针指向一个资源;不支持复制,但是可以将所有权移动;这个智能指针最常用,开销是最小的;

std::unique_ptr<int> ptr1(new int(10));
// 或使用 make_unique(推荐)
auto ptr2 = std::make_unique<int>(10);

// 转移所有权
std::unique_ptr<int> ptr3 = std::move(ptr1);
  1. std::shared_ptr

共享所有权,多个指针可以指向同一个资源,使用引用计数,当最后一个shared_ptr被销毁时,资源会被释放,且支持复制;后续会详细介绍;

  1. std::weak_ptr

先看一段代码:

std::shared_ptr<int> shared = std::make_shared<int>(30);
std::weak_ptr<int> weak = shared;

if(auto locked = weak.lock())
{
	std::cout << *locked << std::endl;
}

配合shared_pt使用,不会增加引用计数,可以用来解决shared_ptr循环引用的问题,可以通过lock()方法获取shared_ptr.

使用建议:

  1. 优先使用unique_ptr,除非确实需要共享所有权
  2. 创建智能指针时优先使用make_unique/make_shared而不是直接new
  3. 使用weak_ptr来避免循环引用
  4. 注意避免悬空指针(比如把原始指针存起来后智能指针释放了资源)

auto_ptr由于存在严重缺陷,不建议使用。在C++98引入,在C++17废除,存在严重缺陷,拷贝时会转移所有权,如下段代码:

// 不推荐使用
std::auto_ptr<int> p1(new int(10));
std::auto_ptr<int> p2 = p1; // p1变为空指针!

下面讨论智能指针的线程安全性:

  1. std::unique_ptr
  • 本身不是线程安全的
  • 多个线程访问同一个unique_ptr需要加锁
  • unique_ptr管理的资源的线程安全性取决于资源本身
  1. std::shared_ptr
  • 引用计数操作是线程安全的(内部使用原子操作)
  • 但对指针指向的数据的访问不是线程安全的
  1. std::weak_ptr
  • 引用计数操作是线程安全的
  • 数据访问不是线程安全的
// 如果需要线程安全,可以配合互斥锁使用
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::mutex mtx;

// 线程安全的访问
{
    std::lock_guard<std::mutex> lock(mtx);
    *ptr = 100;
}

对于shared_ptr进行详细介绍:

2 shared_ptr 线程安全实战

2.1 shared_ptr 线程安全的部分

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

#include <iostream>
#include <thread>
#include <mutex>
#include <vector

// 初始化一个共享的整型变量p,初始值为0
std::shared_ptr<int> p = std::make_shared<int>(0);
// 定义常量N,值为10000,用于确定数组大小
constexpr int N = 10000;
// 初始化两个共享指针数组,用于多线程环境下对共享资源的并发访问
std::vector<std::vector<std::shared_ptr<int>>> sp_arr1(N);
std::vector<std::vector<std::shared_ptr<int>>> sp_arr2(N);

/**
 * 在给定的共享指针数组中增加元素
 *
 * @param sp_arr 要增加元素的共享指针数组
 */
void increment_count(std::vector<std::vector<std::shared_ptr<int>>>& sp_arr)
{
    for (int i = 0; i < N; i++)
    {
        // 在数组的每个位置添加共享指针p
        sp_arr[i].push_back(p);

    }
}

int main() {
    // 测试开始的提示信息
    std::cout << "Shared_thread Test start============ \n " << std::endl;
    // 创建两个线程,分别执行increment_count函数,传入不同的数组
    std::thread t1(increment_count, std::ref(sp_arr1));
    std::thread t2(increment_count, std::ref(sp_arr2));

    // 等待两个线程执行完毕
    t1.join();
    t2.join();
    // 输出共享指针p的引用计数
    std::cout << "p.use_count() = " << p.use_count() << std::endl;
    // 测试结束的提示信息
    std::cout << " \n Shared_thread Test end=================" << std::endl;
    return 0;
}

输出为:

Shared_thread Test start============ 
 
p.use_count() = 20001
 
 Shared_thread Test end=================

初始引用计数:
p 最初有一个引用计数,因为它本身就是一个 std::shared_ptr,所以初始引用计数为 1。

线程 t1 和 t2 的操作:
• 每个线程将 p 赋值给一个包含 10,000 个元素的向量中的每个元素。
• 每次赋值操作都会增加 p 的引用计数。
• 两个线程各自增加 10,000 次引用计数。
• 因此,总的引用计数增加量是 10,000 + 10,000 = 20,000。

最终引用计数:
初始引用计数 1 加上两个线程增加的引用计数 20,000,总计为 1 + 20,000 = 20,001。

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

2.2 线程不安全的部分

  1. 访问对象

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


#include <iostream>
#include <thread>
#include <vector>

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 th1(modify_memory);
    std::thread th2(modify_memory);
    th1.join();
    th2.join();
    std::cout << "p1.use_count() = " << *p1 << std::endl;
    return 0;
}

运行输出:

//第一次运行输出
p1.use_count() = 13615
//第二次运行输出
p1.use_count() = 12074

可以看到输出的结果不是预想的20000,每次输出结果都会发生变化。因此同时修改std::shared_ptr指向的对象并不是线程安全的;

  1. 直接修改std::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

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

#include <iostream>
#include <thread>
#include <vector>
#include <mutex

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);  // 创建一个 shared_ptr
std::mutex mtx;  // 互斥锁

void modifySharedPtr() {
    std::lock_guard<std::mutex> lock(mtx);  // 加锁
    sharedPtr = std::make_shared<int>(100);  // 安全地修改指向
}

int main()
{
    std::thread th1(modifySharedPtr);
    std::thread th2(modifySharedPtr);
    th1.join();
    th2.join();
    std::cout << "p1.use_count() = " << *p1 << std::endl;
    return 0;
}

** 总结**

  • std::shared_ptr 的引用计数操作是线程安全的。
  • std::shared_ptr 所指向的对象的访问需要额外的同步机制(如 std::mutex)来保证线程安全。
  • 直接修改 std::shared_ptr 对象本身的指向在多线程环境中是不安全的,可能导致数据竞争和未定义行为。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值