一、前言
在 C++ 工程中,单例模式与全局对象的管理一直是一个“微妙”的话题。合理的单例可以帮助我们减少资源浪费、避免重复创建,提升性能,但如果处理不当,就会引发 初始化顺序问题(static initialization order fiasco)、内存泄漏、析构顺序问题 等。
Chromium 作为一个庞大而复杂的 C++ 工程,在单例管理上积累了丰富的实践经验。源码中经常出现两类单例工具:
-
base::LazyInstance<T> -
base::NoDestructor<T>
这两者都旨在解决全局对象的生命周期和线程安全问题,但设计理念和使用场景却有所不同。本文将通过源码剖析、案例代码、应用场景对比,帮助大家深入理解两者的差异与实战用法。
二、单例与全局对象的常见问题
在进入正文前,我们先回顾下为什么会有 LazyInstance 和 NoDestructor。
1. 初始化顺序问题
假设我们有多个全局对象:
#include <iostream>
struct A {
A() { std::cout << "A constructor\n"; }
~A() { std::cout << "A destructor\n"; }
};
struct B {
B() { std::cout << "B constructor\n"; }
~B() { std::cout << "B destructor\n"; }
};
A a;
B b;
int main() {
return 0;
}
在不同编译单元(translation unit)中,如果 a 和 b 的初始化顺序无法保证,就可能导致依赖关系错误。这就是著名的 static initialization order fiasco。
2. 析构顺序问题
即使初始化顺序正确,如果对象在进程退出时析构,依赖顺序错误也会导致 use-after-free。例如:
class Logger {
public:
~Logger() { Flush(); } // 假设需要用到其他全局对象
void Log(const std::string& msg) { /* ... */ }
void Flush() { /* ... */ }
};
Logger g_logger;
int main() {
g_logger.Log("hello");
return 0;
}
如果 Flush() 里依赖了一个比 g_logger 更早析构的对象,就会出错。
3. 线程安全问题
全局单例往往需要在多线程环境下使用,初始化和访问必须保证线程安全。手写锁或者双重检查锁(DCLP)容易出错,因此需要更安全的封装。
三、LazyInstance 详解
1. 设计目标
base::LazyInstance<T> 是 Chromium 早期提供的一种 线程安全的懒加载单例 工具,目标是:
-
延迟初始化(第一次使用时才创建对象)。
-
避免初始化顺序问题。
-
提供线程安全的初始化保证。
-
支持自定义析构(可选)。
2. 基本用法
#include "base/lazy_instance.h"
class Foo {
public:
Foo() { /* init */ }
void DoSomething() {}
};
static base::LazyInstance<Foo>::Leaky g_foo = LAZY_INSTANCE_INITIALIZER;
void Test() {
g_foo.Get().DoSomething();
}
这里的 g_foo 是一个全局单例,第一次调用 g_foo.Get() 时才会初始化。
3. 源码解析
LazyInstance 的实现位于 base/lazy_instance.h,其核心是:
-
内部维护一个
base::internal::LeakyLazyInstanceTraits<T>,决定对象是否析构。 -
使用
subtle::AtomicWord state_来保证线程安全的初始化。 -
初始化时采用 原子 CAS + pthread_once/InitOnce 等机制,确保只构造一次。
核心逻辑简化后大致如下:
template <typename T>
class LazyInstance {
public:
T& Get() {
if (!state_) Init();
return *ptr_;
}
private:
void Init() {
// 使用原子操作确保只有一个线程成功初始化
if (InterlockedCompareExchange(&state_, CREATING, 0) == 0) {
ptr_ = new T();
state_ = CREATED;
} else {
// 等待其他线程完成初始化
while (state_ != CREATED) { /* spin */ }
}
}
volatile AtomicWord state_ = 0;
T* ptr_ = nullptr;
};
4. Leaky vs Destructor
LazyInstance 提供了 Leaky 模式:
-
Leaky:对象永远不析构,避免退出时的析构顺序问题。
-
非 Leaky:对象会在退出时析构(可能导致复杂的顺序问题)。
因此,在 Chromium 源码中,我们经常看到 LazyInstance<T>::Leaky 这种用法。
5. 使用场景
-
需要线程安全的懒加载单例。
-
需要延迟初始化,避免静态初始化开销。
-
可以接受“不析构”,或者显式控制析构顺序。
常见例子:g_browser_process、g_instance 等全局单例管理器。
四、NoDestructor 详解
1. 设计目标
随着 C++11 以后标准库提供了 magic static(线程安全局部静态变量),LazyInstance 的存在感逐渐下降。Chromium 引入了 base::NoDestructor<T>,提供一种 简单而高效的全局单例实现。
它的核心思想是:
-
使用局部静态变量(保证线程安全)。
-
禁止析构(对象生命周期贯穿整个进程)。
-
没有复杂的原子 CAS 逻辑,性能更高。
2. 基本用法
#include "base/no_destructor.h"
class Bar {
public:
Bar() { /* init */ }
void DoSomething() {}
};
Bar& GetBar() {
static base::NoDestructor<Bar> instance;
return *instance;
}
void Test() {
GetBar().DoSomething();
}
与 LazyInstance 相比,这种写法更简洁,不需要宏和初始化器。
3. 源码解析
核心实现位于 base/no_destructor.h:
template <typename T>
class NoDestructor {
public:
template <typename... Args>
explicit NoDestructor(Args&&... args) {
new (storage_) T(std::forward<Args>(args)...);
}
T& operator*() { return *reinterpret_cast<T*>(storage_); }
T* operator->() { return reinterpret_cast<T*>(storage_); }
private:
alignas(T) char storage_[sizeof(T)];
};
-
对象直接构造在
storage_内存中。 -
没有析构函数(故名 NoDestructor),对象不会被销毁。
-
利用局部静态保证线程安全初始化。
4. 使用场景
-
单例对象需要贯穿整个进程生命周期。
-
无需在退出时析构(或析构可能引发问题)。
-
代码希望尽量简洁、现代化。
常见例子:GetInstance() 风格的单例。
五、LazyInstance vs NoDestructor 对比
| 特性 | LazyInstance | NoDestructor |
|---|---|---|
| 初始化方式 | 懒加载(第一次调用时初始化) | 懒加载(magic static) |
| 线程安全 | 依赖原子 CAS 和锁 | C++11 标准保证 |
| 析构行为 | 可选:Leaky(不析构)或析构 | 永不析构 |
| 实现复杂度 | 较高,需要宏和模板 traits | 简单,直接封装 |
| 推荐程度 | 逐渐减少使用 | 更推荐,现代化 |
六、实战案例对比
1. 使用 LazyInstance 实现日志系统
#include "base/lazy_instance.h"
#include <iostream>
class Logger {
public:
void Log(const std::string& msg) {
std::cout << msg << std::endl;
}
};
static base::LazyInstance<Logger>::Leaky g_logger = LAZY_INSTANCE_INITIALIZER;
void WriteLog(const std::string& msg) {
g_logger.Get().Log(msg);
}
int main() {
WriteLog("Hello LazyInstance");
return 0;
}
2. 使用 NoDestructor 实现配置管理器
#include "base/no_destructor.h"
#include <string>
#include <iostream>
class ConfigManager {
public:
ConfigManager() : config_("default") {}
void SetConfig(const std::string& c) { config_ = c; }
void Show() { std::cout << "Config: " << config_ << std::endl; }
private:
std::string config_;
};
ConfigManager& GetConfigManager() {
static base::NoDestructor<ConfigManager> instance;
return *instance;
}
int main() {
GetConfigManager().SetConfig("prod");
GetConfigManager().Show();
return 0;
}
3. 性能差异
-
LazyInstance在初始化时需要原子操作和自旋等待,开销略高。 -
NoDestructor仅依赖magic static,更轻量。
七、源码中的实际使用场景
在 Chromium 源码中:
-
LazyInstance更多见于早期代码,例如chrome/browser/...中的全局服务对象。 -
NoDestructor是新代码的首选,比如很多GetInstance()风格的单例函数都使用它。
示例:
// base/command_line.cc
CommandLine* CommandLine::ForCurrentProcess() {
static base::NoDestructor<CommandLine> command_line;
return command_line.get();
}
而 LazyInstance 的典型使用:
// base/message_loop/message_loop.cc
base::LazyInstance<MessageLoop::TaskObserverList>::Leaky
g_task_observers = LAZY_INSTANCE_INITIALIZER;
八、最佳实践与总结
-
新代码优先使用
NoDestructor<T>:简洁、现代、性能更好。 -
遗留代码中可能保留
LazyInstance<T>:避免大规模重构时引入风险。 -
如果对象必须在退出时析构,需谨慎考虑依赖关系,否则建议使用
Leaky或NoDestructor。 -
不要手写单例模式,尽量依赖 Chromium 提供的工具,避免线程安全陷阱。
九、结语
LazyInstance<T> 和 NoDestructor<T> 代表了 Chromium 在不同阶段对 全局单例管理 的思考与演进。前者解决了初始化顺序和线程安全问题,后者则依赖 C++11 特性,简化了实现并提升了性能。
从工程角度来看:
-
LazyInstance 更像是历史产物,适合理解底层实现细节。
-
NoDestructor 则是未来主流,适合写出简洁稳定的现代 C++ 代码。
理解它们的区别与使用场景,有助于我们在实际工程中更好地管理全局对象,避免隐藏的 bug。

3212

被折叠的 条评论
为什么被折叠?



