在C++多线程编程中,std::shared_ptr的延迟初始化是常见需求,既可以减少程序启动时的资源开销,也能避免不必要的对象构造。但多线程并发访问时,如果初始化逻辑没有做好同步,很容易出现重复初始化、内存泄漏或者访问未完全构造对象的问题,双重检查锁模式是优化同步开销的常用手段,不过需要结合std::shared_ptr的特性做正确实现。

为什么需要延迟初始化std::shared_ptr
延迟初始化的核心目标是把对象的构造时机从程序启动或者对象创建时,推迟到第一次真正需要使用该对象的时候。对于std::shared_ptr管理的资源来说,延迟初始化有这些好处:
- 减少程序启动时的资源占用,避免提前构造暂时用不到的大对象
- 如果对象构造可能失败,延迟初始化可以把失败处理放在使用处,逻辑更清晰
- 对于单例场景或者全局共享资源,延迟初始化可以避免初始化顺序带来的问题
传统双重检查锁的问题
很多开发者首先会想到用双重检查锁来实现线程安全的延迟初始化,传统写法如下:
#include <memory>
#include <mutex>
std::shared_ptr<int> g_ptr;
std::mutex g_mutex;
std::shared_ptr<int> get_instance() {
if (!g_ptr) { // 第一次检查,避免每次都加锁
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_ptr) { // 第二次检查,加锁后确认仍未初始化
g_ptr = std::make_shared<int>(100);
}
}
return g_ptr;
}
这种写法看似没有问题,但实际上存在安全隐患。因为g_ptr = std::make_shared<int>(100)这个操作不是原子的,它分为两步:首先在堆上构造int对象并分配控制块,然后将g_ptr的内部指针指向这个对象。在没有内存屏障的情况下,其他线程可能看到g_ptr已经不为空,但指向的对象还没有完成构造,此时访问这个指针就会出现未定义行为。
安全的双重检查锁实现方案
要解决上面的问题,核心是保证std::shared_ptr的赋值操作对其他线程可见时,对象已经完全构造完成。C++11之后标准已经保证了std::shared_ptr的构造函数和赋值操作的相关同步语义,我们可以结合std::call_once或者正确的内存顺序来实现安全版本。
方案一:使用std::call_once实现
std::call_once是C++标准库提供的单次调用工具,本身就保证了线程安全和正确的内存同步,实现起来更简单也更安全:
#include <memory>
#include <mutex>
std::shared_ptr<int> g_ptr;
std::once_flag g_flag;
std::shared_ptr<int> get_instance() {
std::call_once(g_flag, []() {
g_ptr = std::make_shared<int>(100);
});
return g_ptr;
}
这种方案不需要手动写双重检查逻辑,std::call_once会保证初始化逻辑只执行一次,并且初始化完成后的结果对其他线程立即可见,完全避免了竞态条件。
方案二:正确的双重检查锁实现
如果一定要使用双重检查锁模式,需要保证指针赋值的同步语义,正确的实现如下:
#include <memory>
#include <mutex>
#include <atomic>
// 使用原子shared_ptr,C++20引入,之前的版本可以用atomic_load/atomic_store
std::atomic<std::shared_ptr<int>> g_atomic_ptr;
std::mutex g_mutex;
std::shared_ptr<int> get_instance() {
// 原子加载,获取当前指针
std::shared_ptr<int> local_ptr = std::atomic_load(&g_atomic_ptr);
if (!local_ptr) {
std::lock_guard<std::mutex> lock(g_mutex);
local_ptr = std::atomic_load(&g_atomic_ptr);
if (!local_ptr) {
local_ptr = std::make_shared<int>(100);
// 原子存储,保证其他线程能看到完整的对象
std::atomic_store(&g_atomic_ptr, local_ptr);
}
}
return local_ptr;
}
这里使用std::atomic包装std::shared_ptr,通过std::atomic_load和std::atomic_store进行读写,这两个操作会插入必要的内存屏障,保证对象构造完成后再被其他线程看到,从而解决传统双重检查锁的内存可见性问题。
两种方案的选择建议
实际开发中更推荐使用std::call_once的方案,原因有两点:
- 代码更简洁,不需要手动处理原子操作和锁的配合,出错概率更低
- 标准库保证
std::call_once的行为,可移植性更好,不需要依赖编译器对std::shared_ptr原子操作的支持
如果是在不支持C++20且无法使用std::atomic<std::shared_ptr>的旧环境中,也可以手动实现基于互斥锁的普通双重检查,但要保证初始化完成后才赋值给共享的std::shared_ptr,并且赋值操作在锁的保护范围内,不过这种方式的同步开销会比原子操作稍大。
注意事项
使用延迟初始化的std::shared_ptr时还要注意几个问题:
- 延迟初始化的对象生命周期要符合预期,避免提前释放导致悬空指针
- 如果初始化可能失败,要在初始化逻辑中处理失败情况,避免返回空的
std::shared_ptr给调用方 - 不要在析构函数中做延迟初始化的反向操作,避免析构顺序带来的问题
需要特别注意的是,普通的std::shared_ptr读写不是线程安全的,只有对同一个std::shared_ptr实例的并发读是安全的,并发写或者同时读写都需要同步,这也是我们上面方案中加入锁和原子操作的原因。
std::shared_ptr延迟初始化双重检查锁C++智能指针修改时间:2026-07-10 16:48:40