欢迎关注本公众号,专注面试题拆解
分享一套视频课程:《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 的指向时,另一个线程也在修改它,可能会导致引用计数不一致,从而导致内存泄漏或双重释放。
未定义行为:访问已释放的内存或访问无效的指针。
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_ptr
std::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电子书资料赠送
精彩文章合集
专题推荐