使用 shared_ptr 管理多线程内存
本节阅读量:
在多线程程序中,内存管理是一个棘手的问题。一个对象可能被多个线程同时访问,甚至在某个线程使用的同时,另一个线程想要销毁它。这就好比多人共用一本书,如果有人在看的时候另一个人把书扔了,就会出问题。
多线程内存管理的困境
考虑这样一个场景:主线程创建了一个对象,传递给工作线程使用,然后主线程继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <iostream>
#include <thread>
class Data {
public:
Data(int v) : value(v) {}
void process() { std::cout << "Processing " << value << "\n"; }
int value;
};
void worker(Data* data) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
data->process(); // 危险!data 可能已经被销毁 - use-after-free 错误
}
int main() {
Data* data = new Data(42);
std::thread t(worker, data);
// 主线程继续执行,完成后删除 data
delete data; // 工作线程可能还在使用 data!
t.join();
return 0;
}
|
这段代码存在严重的 use-after-free 问题:
- 主线程创建了
Data 对象
- 工作线程开始执行,但睡眠了 100ms
- 主线程立即
delete data
- 工作线程醒来后访问已经被释放的内存,导致未定义行为(可能崩溃或数据损坏)
使用 shared_ptr 解决问题
std::shared_ptr 使用引用计数来管理对象的生命周期。当最后一个 shared_ptr 被销毁时,对象才会被删除。这在多线程环境下非常安全,因为引用计数的操作是原子的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
#include <thread>
#include <memory>
class Data {
public:
Data(int v) : value(v) { std::cout << "Data created\n"; }
~Data() { std::cout << "Data destroyed\n"; }
void process() { std::cout << "Processing " << value << "\n"; }
int value;
};
void worker(std::shared_ptr<Data> data) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
data->process(); // 安全!shared_ptr 保证对象存活
}
int main() {
{
std::shared_ptr<Data> data = std::make_shared<Data>(42);
std::thread t(worker, data);
t.detach(); // 分离线程,主线程继续执行(注意:实际项目中应谨慎使用detach)
// data 在这里离开作用域,但对象不会被销毁
// 因为工作线程还持有 shared_ptr 的副本
std::cout << "Main thread continues...\n";
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::cout << "Main thread exit\n";
return 0;
}
|
输出:
1
2
3
4
5
|
Data created
Main thread continues...
Processing 42
Data destroyed
Main thread exit
|
即使主线程的 data 离开了作用域,对象也不会被销毁,因为工作线程还持有一个副本。只有当工作线程完成并销毁它的 shared_ptr 后,引用计数归零,对象才会被安全地销毁。
注意 shared_ptr 本身的线程安全性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <iostream>
#include <thread>
#include <memory>
#include <vector>
std::shared_ptr<int> global_ptr;
std::mutex mtx;
void unsafe_writer() {
// 危险!多个线程同时修改 global_ptr,不是线程安全的
global_ptr = std::make_shared<int>(42);
}
void safe_writer() {
// 安全:使用锁保护 shared_ptr 的修改
std::lock_guard<std::mutex> lock(mtx);
global_ptr = std::make_shared<int>(42);
}
void reader() {
std::shared_ptr<int> local_copy;
{
std::lock_guard<std::mutex> lock(mtx);
local_copy = global_ptr; // 拷贝 shared_ptr 需要加锁
}
// 现在可以安全地使用 local_copy,不需要锁
if (local_copy) {
std::cout << "Value: " << *local_copy << "\n";
}
}
|
关键点:
shared_ptr 的控制块(包含引用计数)的操作是线程安全的(原子操作)
- 但
shared_ptr 对象本身的读写不是线程安全的
- 多个线程同时读写同一个
shared_ptr 对象需要加锁保护
重要注意事项
1. 性能开销
shared_ptr 的引用计数操作有性能开销,不适合性能敏感的场景。在单线程或所有权明确的情况下,优先考虑 unique_ptr。
2. 循环引用问题
1
2
3
4
5
6
7
8
9
10
11
12
|
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 循环引用!
};
// 使用 weak_ptr 解决循环引用
class SafeNode {
public:
std::shared_ptr<SafeNode> next;
std::weak_ptr<SafeNode> prev; // 使用 weak_ptr 避免循环引用
};
|
3. 优先使用 make_shared
std::make_shared 相比 new + shared_ptr 构造函数:
- 更高效:一次内存分配同时分配对象和控制块
- 更安全:避免异常安全问题
- 更简洁:代码更清晰
最佳实践
- 生命周期管理:当对象需要被多个线程共享时使用
shared_ptr
- 线程安全:
shared_ptr 保证对象生命周期安全,但对象本身的线程安全性需单独处理
- 性能考虑:在性能敏感场景评估引用计数的开销
- 避免循环引用:使用
weak_ptr 打破循环引用
- 优先使用 make_shared:更高效、更安全
- 所有权明确时用 unique_ptr:当所有权单一且明确时,
unique_ptr 是更好的选择