并发编程中std::make_shared的正确打开方式:保证线程安全的智能指针技巧
发布时间: 2024-10-23 10:05:58 阅读量: 22 订阅数: 25
![并发编程中std::make_shared的正确打开方式:保证线程安全的智能指针技巧](https://img-blog.csdnimg.cn/20210620161412659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3h1bnllX2RyZWFt,size_16,color_FFFFFF,t_70)
# 1. 并发编程中的智能指针简介
在现代编程世界中,智能指针是管理资源的重要工具。特别是在并发编程的背景下,合理利用智能指针能够有效避免内存泄漏和线程安全问题。在C++中,标准模板库(STL)提供的智能指针类型,如std::unique_ptr、std::shared_ptr和std::weak_ptr,被广泛应用于资源管理和对象生命周期控制。本章将为您概述智能指针在并发编程中的作用,并为后续章节中深入探讨std::shared_ptr和std::make_shared做准备。
智能指针之所以在并发编程中受到青睐,主要是因为它们可以自动管理内存,减少手动分配和释放内存的错误。std::shared_ptr通过引用计数的方式允许多个线程共享同一资源,且只有在最后一个引用被销毁时才释放资源,这大大降低了因线程同步问题导致的资源泄露风险。但智能指针并非万能钥匙,它们在设计和使用时也有诸多细节需要关注,如引用计数的线程安全、自定义删除器的实现等。
本章后续部分将详细解释并发编程中智能指针的概念和类型,并通过实例演示其使用方法,帮助读者掌握智能指针在多线程编程中的基础应用。
# 2. std::shared_ptr的基础知识
## 2.1 std::shared_ptr的基本使用
### 2.1.1 std::shared_ptr的构造和析构机制
`std::shared_ptr`是C++11引入的一种智能指针,用于自动管理动态分配对象的生命周期。当最后一个`std::shared_ptr`对象被销毁或者重置时,它指向的对象会被自动删除。
```cpp
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "Constructing MyClass\n"; }
~MyClass() { std::cout << "Destructing MyClass\n"; }
};
int main() {
std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>(); // 构造
std::shared_ptr<MyClass> p2 = p1; // p1和p2共享一个指针
std::shared_ptr<MyClass> p3; // 默认构造
p3 = p2; // p3也指向p1和p2共享的同一个对象
return 0; // 析构函数被调用,p1,p2,p3都被销毁
}
```
在上述代码中,`std::make_shared`创建了一个`MyClass`实例,并返回了一个指向它的`std::shared_ptr`对象。通过赋值操作,`p2`和`p3`共享了`p1`的指针。当`main`函数结束时,`p1`、`p2`、和`p3`都会被销毁,这时`MyClass`的析构函数会被调用一次,证明了析构也是共享的。
### 2.1.2 std::shared_ptr的引用计数原理
`std::shared_ptr`维护一个引用计数来跟踪有多少`std::shared_ptr`实例共享同一个对象。当最后一个`std::shared_ptr`被销毁时,引用计数变为零,对象被删除。
```cpp
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(10); // 引用计数为1
std::shared_ptr<int> p2 = p1; // 引用计数增加到2
std::cout << "p1.use_count() = " << p1.use_count() << '\n'; // 输出引用计数
return 0;
}
```
在这个例子中,`p1`初始化为指向一个动态分配的整数,并将引用计数设置为1。将`p1`赋值给`p2`后,引用计数增加到2,因为现在有两个指针共享该对象。在函数末尾,`p1`和`p2`都会被销毁,引用计数恢复为0,相关资源被释放。
## 2.2 std::shared_ptr的线程安全特性
### 2.2.1 线程安全的资源共享问题
在并发环境下,共享资源的访问需要特别注意线程安全问题。`std::shared_ptr`能够安全地在多个线程之间共享资源,因为它的引用计数操作是原子的。
```cpp
#include <iostream>
#include <memory>
#include <thread>
int main() {
std::shared_ptr<int> p = std::make_shared<int>(42);
std::thread t1([&p]() { std::cout << *p << '\n'; });
std::thread t2([&p]() { std::cout << *p << '\n'; });
t1.join();
t2.join();
std::cout << "use_count = " << p.use_count() << std::endl;
return 0;
}
```
上面的代码演示了在两个不同的线程中同时访问同一个`std::shared_ptr`对象,由于`std::shared_ptr`的内部引用计数机制是线程安全的,所以即使多个线程同时对引用计数进行修改,也不会导致不一致的问题。
### 2.2.2 std::shared_ptr的原子引用计数机制
为了支持线程安全,`std::shared_ptr`内部实现使用了原子操作来更新引用计数。原子操作保证了即使是多个线程同时访问,引用计数的更新也是安全的。
```cpp
#include <iostream>
#include <memory>
#include <thread>
#include <atomic>
void thread_function(std::shared_ptr<int> sp, std::atomic<int>& count) {
for (int i = 0; i < 10000; ++i) {
sp.use_count();
++count;
}
}
int main() {
std::shared_ptr<int> shared_int = std::make_shared<int>(0);
std::atomic<int> count = 0;
std::thread t1(thread_function, shared_int, std::ref(count));
std::thread t2(thread_function, shared_int, std::ref(count));
t1.join();
t2.join();
std::cout << "Final use count is " << shared_int.use_count()
<< " and atomic counter is " << count << std::endl;
}
```
在这个例子中,两个线程尝试同时增加一个原子计数器和`std::shared_ptr`的使用计数。即使在这种高竞争的条件下,引用计数的准确性依旧保持不变,这归功于`std::shared_ptr`内部使用的原子操作。
## 2.3 std::shared_ptr的高级特性
### 2.3.1 自定义删除器
有时需要控制对象的销毁行为,特别是在自定义内存管理或者资源释放逻辑时。`std::shared_ptr`允许你提供一个自定义删除器。
```cpp
#include <iostream>
#include <memory>
void myDeleter(int* p) {
std::cout << "Deleting the object pointed by " << p << '\n';
delete p;
}
int main() {
std::shared_ptr<int> p = std::shared_ptr<int>(new int(10), myDeleter);
return 0;
}
```
在这个例子中,我们提供了一个自定义的删除器`myDeleter`,它会在`std::shared_ptr`对象被销毁时被调用,从而替代默认的`delete`操作。
### 2.3.2 std::weak_ptr的使用场景和优势
`std::weak_ptr`是`std::shared_ptr`的补充,它指向`std::shared_ptr`管理的对象,但它不拥有对象。当最后一个`std::shared_ptr`被销毁后,使用`std::weak_ptr`可能得到一个空的弱指针。
```cpp
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> shared_ptr = std::make_shared<int>(10);
std::weak_ptr<int> weak_ptr(shared_ptr);
std::cout << "weak_ptr.use_count() = " << weak_ptr.use_count() << '\n'; // 输出与shared_ptr相同的引用计数
std::cout << "weak_ptr.expired() = " << weak_ptr.expired() << std::endl; // 返回是否已过期
shared_ptr.reset();
std::cout << "After shared_ptr is reset:\n";
std::cout << "weak_ptr.use_count() = " << weak_ptr.use_count() << '\n';
std::cout << "weak_ptr.expired() = " << weak_ptr.expired() << std::endl;
return 0;
}
```
代码说明`std::weak_ptr`不增加引用计数,用于解决`std::shared_ptr`可能产生的循环引用问题。当`std::shared_ptr`被重置后,`std::weak_ptr`可以继续存在,但调用`expired()`将返回`true`,表示所指向的对象已经不存在了。
通过本章节的介绍,我们可以看到`std::shared_ptr`提供了多种机制以支持更安全和方便的资源共享和内存管理。理解它的基本使用、线程安全特性和高级特性,对于进行并发编程和资源管理是非常关键的。
# 3. std::make_shared的并发优势
## 3.1 std::make_shared与std::shared_ptr的区别
### 3.1.1 动态内存分配的效率对比
在并发编程中,智能指针的使用不仅涉及资源管理和生命周期控制,还关联到内存分配的效率。`std::make_shared` 和 `std::shared_ptr` 在这一点上有显著的区别。`std::make_shared` 是一个函数模板,它会在单次内存分配中创建一个控制块和一个对象实例。这与直接使用 `std::shared_ptr` 构造函数相比,后者在每次对象构造时都需要进行内存分配。
在多线程环境下,单次内存分配的优势尤为明显。单次内存分配可以减少内存碎片的产生,提高内存分配的效率,尤其是在分配大量小对象时。这种分配方式同样减少了线程间同步的开销,因为多个线程可以共享这一块内存而不需要单独的控制块。
```cpp
// 示例代码:std::make_shared的内存分配效率优势
#include <iostream>
#include <memory>
int main() {
// 创建一个std::shared_ptr对象,涉及两次内存分配
std::shared_ptr<int> ptr1(new int(10));
// 创建一个std::make_shared对象,只进行一次内存分配
auto ptr2 = std::make_shared<int>(10);
// 输出两次内存分配的大小对比
std::cout << "Memory allocated for ptr1: " << sizeof(ptr1) << std::endl;
std::cout << "Memory allocated for ptr2: " << sizeof(ptr2) << std::endl;
return 0;
}
```
在上述代码中,`ptr1` 和 `ptr2` 分别代表通过不同方式创建的智能指针。通过输出内存分配大小的对比,我们可以看到使用 `std::make_shared` 通常会更加高效。当然,这里只是简化的对比,实际上 `std::make_shared` 的优势还会体现在减少构造函数调用次数和避免空悬指针等方面。
### 3.1.2 std::make_shared的资源回收策略
`std::make_shared` 不仅在内存分配上表现出优势,在资源回收方面也有独到之处。当最后一个 `std::shared_ptr` 被销毁时,它所管理的内存会立即被释放。由于 `std::make_shar
0
0