当前位置: 首页 > news >正文

Chromium 源码中的单例管理:LazyInstance 与 NoDestructor 的深入解析与实战对比

一、前言

在 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)中,如果 ab 的初始化顺序无法保证,就可能导致依赖关系错误。这就是著名的 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_processg_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 对比

特性LazyInstanceNoDestructor
初始化方式懒加载(第一次调用时初始化)懒加载(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>::Leakyg_task_observers = LAZY_INSTANCE_INITIALIZER;

八、最佳实践与总结

  1. 新代码优先使用 NoDestructor<T>:简洁、现代、性能更好。

  2. 遗留代码中可能保留 LazyInstance<T>:避免大规模重构时引入风险。

  3. 如果对象必须在退出时析构,需谨慎考虑依赖关系,否则建议使用 LeakyNoDestructor

  4. 不要手写单例模式,尽量依赖 Chromium 提供的工具,避免线程安全陷阱。


九、结语

LazyInstance<T>NoDestructor<T> 代表了 Chromium 在不同阶段对 全局单例管理 的思考与演进。前者解决了初始化顺序和线程安全问题,后者则依赖 C++11 特性,简化了实现并提升了性能。

从工程角度来看:

  • LazyInstance 更像是历史产物,适合理解底层实现细节。

  • NoDestructor 则是未来主流,适合写出简洁稳定的现代 C++ 代码。

理解它们的区别与使用场景,有助于我们在实际工程中更好地管理全局对象,避免隐藏的 bug。

http://www.dtcms.com/a/350610.html

相关文章:

  • vscode(MSVC)进行c++开发的时,在debug时查看一个eigen数组内部的数值
  • uniapp安卓真机调试问题解决总结
  • redis----list详解
  • C# 相机内存复用(减少图像采集耗时)以及行数复用
  • 自定义树形构造器
  • python项目实战 3D宠物狗
  • 关于传统的JavaWeb(Servlet+Mybatis)项目部署Tomcat后的跨域问题解决方案
  • MM-2025 | 北航双无人机协作助力视觉语言导航!AeroDuo:基于空中双机系统的无人机视觉语言导航
  • 简述mysql中索引类型有哪些,以及对数据库的性能的影响?
  • JBL音响代理——河北正娱科技的声学精品工程
  • 网络编程-HTTP
  • 插曲 - 为什么光速不变
  • 【代码】洛谷P3391 【模板】文艺平衡树(FHQ Treap)
  • 低质量视频变高清AI:告别模糊,重现清晰画质
  • chrome插件开发(二)
  • vue家教预约平台设计与实现(代码+数据库+LW)
  • 驱动-热插拔-Netlink广播监听内核状态
  • HarmonyOS实战(DevEco AI篇)—CodeGenie + DeepSeek构建鸿蒙开发的超级外挂工作流
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十九)子窗口
  • 您的连接不是私密连接问题解决
  • 借Copilot之力,实现办公效率的跃升
  • 数据库原理及应用_数据库基础_第2章关系数据库标准语言SQL_索引和视图
  • 软件使用教程(二):VS Code的Copilot、Git设置与使用
  • 复制和下载飞书文档的方法教程
  • Unity开发如何实现换装技术
  • Ubuntu 14.10 i386桌面版安装教程(U盘启动详细步骤-附安装包下载)​
  • LeetCode 100题(3)(10题)
  • 实用电脑小工具分享,守护电脑隐私与提升效率21/64
  • CANopen - DCF(Device Configuration File) 介绍
  • 平安产险青海分公司助力国家电投黄河公司安全生产