使用 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 问题:

  1. 主线程创建了 Data 对象
  2. 工作线程开始执行,但睡眠了 100ms
  3. 主线程立即 delete data
  4. 工作线程醒来后访问已经被释放的内存,导致未定义行为(可能崩溃或数据损坏)

使用 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 构造函数:

  • 更高效:一次内存分配同时分配对象和控制块
  • 更安全:避免异常安全问题
  • 更简洁:代码更清晰


0.9 线程本地存储

上一节

常用库

下一节