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>::Leaky
    g_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。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ปรัชญา แค้วคำมูล

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值