C++模板实现线程安全单例模式:原理、实现与工程实践
1. 项目概述
在C++开发中,单例模式(Singleton Pattern)是一个老生常谈却又常谈常新的设计模式。它的核心目标很明确:确保一个类在整个程序生命周期内只有一个实例,并提供一个全局访问点。听起来简单,对吧?但当你把它扔进多线程的熔炉里,事情就变得复杂起来了。线程A刚判断完实例指针为空,还没来得及创建,线程B也冲进来做了同样的判断,得,两个线程都认为自己该创建实例,单例瞬间就变成了“双例”,数据竞争、资源浪费的问题接踵而至。这就是为什么“线程安全”会成为单例模式讨论中绕不开的坎。
而C++模板(Template),作为C++泛型编程的利器,其核心价值在于代码复用和类型安全。我们经常用模板来写容器、算法,但你是否想过,用它来封装单例模式?这可不是简单的炫技。通过模板,我们可以将单例的核心逻辑(如实例创建、线程同步)抽象出来,形成一个通用的“单例生成器”。任何类,只要继承或使用这个模板,就能自动获得一个线程安全的单例,而无需在每个类里重复编写加锁、双检锁(Double-Checked Locking)那些繁琐又容易出错的代码。这不仅仅是少写几行代码的问题,更是将最佳实践固化,从根本上避免因手动实现疏忽而引入的bug。
所以,当我们谈论“用C++模板实现线程安全的单例”时,我们实际上是在追求两个目标的结合:通用性(通过模板,一劳永逸)和可靠性(通过严谨的线程安全设计,应对高并发场景)。这就像为你所有的单例需求打造了一个即插即用、经过严格测试的“安全插座”,而不是每次都需要自己冒着风险去接电线。接下来,我们就深入这个“插座”的内部,看看它是如何被设计和制造出来的。
2. 单例模式的核心挑战与常见实现剖析
在动手用模板造轮子之前,我们必须先彻底理解单例模式面临的几个核心挑战,以及传统实现是如何应对的。这能帮助我们明白,模板最终要解决的是什么问题。
2.1 单例模式的基本要求与经典分类
一个合格的、教科书式的单例模式,通常需要满足以下几点:
- 私有化构造函数、拷贝构造函数和赋值运算符:这是防止外部通过
new、拷贝或赋值来创建新实例的第一道防线。 - 提供一个静态的公共方法作为全局访问点:通常是
GetInstance()或Instance()。 - 在类内部保存该类的唯一静态实例指针或引用。
根据实例创建的时机,单例模式主要分为两类:
- 懒汉式(Lazy Initialization):只有在第一次调用
GetInstance()时,才创建实例。优点是延迟加载,如果程序从头到尾都没用到这个单例,那它就不会被创建,节省资源。缺点就是在多线程环境下,需要额外的同步控制来保证只创建一次,否则就是线程不安全的。 - 饿汉式(Eager Initialization):在程序启动时、
main函数执行之前,就完成实例的初始化(通常通过全局静态变量或类静态成员初始化)。优点是实现简单,天生线程安全,因为初始化发生在任何线程启动之前。缺点是不管用不用,实例都会创建,可能造成不必要的资源占用和启动时间延长。
2.2 线程安全问题的根源与演进方案
懒汉式是线程安全问题的重灾区。我们来看一个最简单的、线程不安全的懒汉式实现:
class UnsafeSingleton { public: static UnsafeSingleton* GetInstance() { if (instance_ == nullptr) { // 危险区:多个线程可能同时进入这个判断 instance_ = new UnsafeSingleton(); } return instance_; } // ... 其他私有化声明 private: static UnsafeSingleton* instance_; }; UnsafeSingleton* UnsafeSingleton::instance_ = nullptr;问题就出在if (instance_ == nullptr)这一行。假设两个线程T1和T2同时首次调用GetInstance(),它们可能都读到instance_为nullptr,然后都执行new操作,单例就被创建了两次。
为了解决这个问题,开发者们想出了几种方案,其演进过程本身就很有启发性:
简单粗暴法:全局锁最直接的想法是在
GetInstance()方法入口处直接加锁。static Singleton* GetInstance() { std::lock_guard<std::mutex> lock(mutex_); // 每次调用都加锁 if (instance_ == nullptr) { instance_ = new Singleton(); } return instance_; }优点:绝对线程安全。缺点:性能杀手。即使实例已经创建,后续成千上万次的调用也依然要争夺这把锁,造成了完全不必要的性能开销。
性能优化法:双检锁(Double-Checked Locking, DCLP)为了减少加锁开销,双检锁应运而生。其核心思想是:只在实例未创建时进行同步,创建之后,所有线程都走无锁的快路径。
static Singleton* GetInstance() { if (instance_ == nullptr) { // 第一次检查,避免不必要的锁竞争 std::lock_guard<std::mutex> lock(mutex_); if (instance_ == nullptr) { // 第二次检查,确保只有一个线程创建实例 instance_ = new Singleton(); } } return instance_; }优点:大部分情况下性能优异。缺点:在C++11标准之前,这个写法存在内存序(Memory Order)问题,可能导致某个线程拿到一个未完全构造好的对象指针。因为
instance_ = new Singleton();这行代码并非原子操作,它可能被分解为:1) 分配内存,2) 构造对象,3) 将地址赋值给instance_。编译器或CPU可能对步骤2和3进行重排序,导致另一个线程在第一次检查时看到一个非空的instance_,但指向的对象却还没构造完成,从而引发未定义行为。C++11引入了std::atomic和内存模型后,可以通过std::atomic<Singleton*>配合std::memory_order来正确实现DCLP,但实现起来变得复杂。现代简洁法:局部静态变量(Meyers‘ Singleton)C++11标准规定,局部静态变量的初始化是线程安全的。这催生了一种极其优雅的实现:
static Singleton& GetInstance() { static Singleton instance; // C++11保证此初始化只发生一次,且线程安全 return instance; }优点:代码极其简洁,线程安全由语言标准保障,是C++11之后的首推荐方式。缺点:存在一些细微的“问题”。首先,它的生命周期始于第一次调用
GetInstance(),终于程序结束。这通常不是问题。其次,在某些特定场景下(如需要依赖其他静态变量析构顺序时),可能会遇到静态变量初始化顺序问题(但单例本身也是静态的,所以这个问题是所有静态变量共有的)。最后,它无法传递构造参数(因为调用点没有参数),不过单例通常也不需要运行时参数。传统稳定法:饿汉式如前所述,在程序启动前初始化,一劳永逸。
class EagerSingleton { private: static EagerSingleton* instance_; }; // 在文件作用域初始化,早于main函数 EagerSingleton* EagerSingleton::instance_ = new EagerSingleton();优点:简单,线程安全。缺点:可能造成启动延迟和资源浪费;无法处理依赖其他单例的复杂初始化场景(因为静态初始化顺序未定义)。
注意:选择哪种方案,取决于你的具体需求。如果对启动性能敏感,且单例使用频率不高,懒汉式(尤其是Meyers‘ Singleton)是优选。如果单例很简单,且确定程序一定会用到,饿汉式也无妨。而双检锁,在需要为单例传递初始化参数等Meyers‘ Singleton无法满足的复杂场景下,仍有其用武之地。
3. 基于模板的通用单例模式设计
分析了这么多,我们发现,无论是双检锁的谨慎,还是局部静态变量的优雅,其代码模式都是高度可复用的。每次为新类实现单例,都要把私有构造函数、静态实例、GetInstance方法等重写一遍,不仅枯燥,更容易出错。这时,模板的优势就凸显出来了。我们可以将单例的核心逻辑抽象成一个模板类,让目标类通过继承或组合的方式来获得单例能力。
3.1 设计目标与架构选择
我们的模板单例设计需要达成以下几个目标:
- 通用性:适用于绝大多数需要单例的类。
- 线程安全:默认提供健壮的线程安全保证。
- 易用性:使用方式尽可能简单直观。
- 灵活性:允许选择不同的单例实现策略(如饿汉式、懒汉式)。
- 生命周期可控:提供可选的、明确的手动销毁接口。
常见的架构有两种:
- 继承(CRTP模式):让目标类继承自一个以自身为模板参数的模板基类。例如
class MyClass : public Singleton<MyClass>。这种方式将单例逻辑紧密绑定到目标类,GetInstance()返回的是MyClass*,类型明确。 - 组合(使用友元):创建一个独立的模板单例管理器,目标类将其声明为友元,以便管理器能访问其私有构造函数。这种方式更松散,但可能需要更多的样板代码。
我们将采用更常见、也更直观的CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)继承方式。CRTP的核心是:在模板基类中,使用派生类作为模板参数。这样,基类就能“知道”派生类的类型。
3.2 基于局部静态变量(Meyers‘ Singleton)的模板实现
这是目前最推荐、最简洁的实现。我们直接利用C++11的线程安全静态局部变量特性。
// Singleton.hpp #ifndef SINGLETON_HPP #define SINGLETON_HPP #include <type_traits> template <typename T> class Singleton { public: // 获取单例实例的引用 static T& GetInstance() noexcept(std::is_nothrow_constructible<T>::value) { static T instance; // 线程安全的初始化点 return instance; } // 删除拷贝构造和赋值操作 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; protected: // 允许派生类构造和析构 Singleton() = default; virtual ~Singleton() = default; // 虚析构,确保派生类对象能被正确析构 }; #endif // SINGLETON_HPP使用方式:
// MyClass.hpp #include "Singleton.hpp" class MyClass : public Singleton<MyClass> { // 必须将基类声明为友元,以便基类的`GetInstance`能调用此类的私有构造函数 friend class Singleton<MyClass>; public: void DoSomething() { /* ... */ } private: // 构造函数私有化,防止外部创建 MyClass() { /* 初始化操作 */ } // 也可以有参数的私有构造函数,但需要通过特化或其他方式传递参数(稍后讨论) }; // 在代码中使用 int main() { MyClass& obj1 = MyClass::GetInstance(); MyClass& obj2 = MyClass::GetInstance(); // obj1 和 obj2 是同一个实例 obj1.DoSomething(); // 无需手动删除,实例在程序退出时自动析构 return 0; }实现解析与注意事项:
noexcept说明符:我们使用了std::is_nothrow_constructible来推断T的构造函数是否不抛出异常,并据此修饰GetInstance。这是一个良好的实践,有助于编译器优化和异常安全。- 保护(Protected)的构造函数和析构函数:它们被设为
protected而不是private,是因为派生类(即MyClass)需要能够调用基类的构造函数和析构函数。同时,protected也阻止了外部直接创建Singleton<T>的实例。 - 虚析构函数:虽然在这个简单场景下可能不是必须的(因为我们通常通过
MyClass::GetInstance()获取引用,而不是操作Singleton<MyClass>*),但这是一个好习惯。如果未来有通过基类指针操作单例的需求,或者有更复杂的继承层次,虚析构能确保资源被正确释放。 friend声明:这是关键!Singleton<T>::GetInstance()内部的static T instance;需要调用T的默认构造函数。由于我们将MyClass的构造函数设为private以实现封装,就必须将Singleton<MyClass>声明为MyClass的友元,授予其访问私有构造函数的权限。- 返回引用:
GetInstance()返回的是T&,而不是T*。这强调了单例始终存在的语义,避免了返回指针可能为nullptr的歧义(尽管在我们实现中不会),也使得语法上更简洁(使用.而非->)。
实操心得:这种实现简单到几乎“无聊”,但恰恰是它的优点。在C++11及以后的环境中,应将其作为默认选择。你可能会在网上看到一些讨论,关于静态局部变量在某些平台特定编译器设置下的潜在问题,但在符合标准的编译器和默认设置下,它的线程安全性是有保障的。
3.3 支持自定义销毁与带参数构造的模板增强
标准的Meyers‘ Singleton虽然好,但有两个局限性:1) 无法手动控制销毁时机;2) 无法在获取实例时传递构造参数。对于某些资源管理严格的场景(如需要明确在某个模块关闭时释放单例资源),或者单例构造依赖运行时配置的场景,我们需要更强大的版本。
方案一:基于指针和std::call_once的懒汉式模板std::call_once是C++11提供的另一个线程安全的一次性初始化工具,配合std::once_flag使用。我们可以用它来安全地创建指针。
// SingletonLazy.hpp #include <memory> #include <mutex> #include <utility> // for std::forward template <typename T> class SingletonLazy { public: // 获取单例指针(支持传递参数构造) template<typename... Args> static T* GetInstance(Args&&... args) { std::call_once(once_flag_, [&]() { instance_.reset(new T(std::forward<Args>(args)...)); }); return instance_.get(); } // 获取单例引用(便捷版本,要求实例已存在或可默认构造) static T& GetInstanceRef() { return *GetInstance(); // 调用无参版本 } // 手动销毁单例(谨慎使用!) static void DestroyInstance() { if (instance_) { instance_.reset(); // 注意:销毁后once_flag_依然被设置,无法再次创建。 // 如果需要重新创建,需要更复杂的设计(如重置once_flag_)。 } } SingletonLazy(const SingletonLazy&) = delete; SingletonLazy& operator=(const SingletonLazy&) = delete; protected: SingletonLazy() = default; virtual ~SingletonLazy() = default; private: static std::unique_ptr<T> instance_; static std::once_flag once_flag_; }; // 静态成员初始化 template<typename T> std::unique_ptr<T> SingletonLazy<T>::instance_; template<typename T> std::once_flag SingletonLazy<T>::once_flag_;使用方式:
class ConfigManager : public SingletonLazy<ConfigManager> { friend class SingletonLazy<ConfigManager>; public: void LoadConfig(const std::string& path) { /* ... */ } const std::string& GetValue(const std::string& key) const { /* ... */ } private: // 私有构造函数,可以接受参数 ConfigManager() = default; // 默认构造 explicit ConfigManager(const std::string& defaultPath) { LoadConfig(defaultPath); } }; int main() { // 第一次调用,传递参数构造 ConfigManager* mgr = ConfigManager::GetInstance("default.cfg"); mgr->GetValue("some_key"); // 后续调用,返回已创建的实例 ConfigManager& mgrRef = ConfigManager::GetInstanceRef(); // 在程序特定点手动销毁(例如,在热重载配置时) // ConfigManager::DestroyInstance(); // ... 然后可以再次调用GetInstance重新创建 return 0; }实现解析与注意事项:
std::call_once与std::once_flag:std::call_once保证其可调用对象(lambda)只被执行一次,即使在多线程环境下。std::once_flag是与之配合使用的标志。这是比手动双检锁更现代、更安全的替代方案。- 可变参数模板与完美转发:
template<typename... Args> static T* GetInstance(Args&&... args)使用了可变参数模板和完美转发(std::forward),可以接受任意数量、任意类型的参数,并将其原封不动地传递给T的构造函数。这提供了极大的灵活性。 std::unique_ptr管理资源:使用智能指针管理单例对象的内存,避免了手动new/delete,更安全。reset()方法用于释放所有权。- 手动销毁的陷阱:
DestroyInstance()提供了手动销毁的能力,但必须极其谨慎。一旦销毁,由于std::once_flag已被标记为“已执行”,再次调用GetInstance()将不会创建新对象(除非once_flag_被重置,但这通常不是线程安全的)。因此,这个功能通常用于“单次销毁,永不重建”或程序明确结束的场景。如果需要在生命周期内重新创建单例,需要更复杂的设计,可能涉及原子变量和新的once_flag。
方案二:饿汉式模板饿汉式模板实现起来更简单,因为它不涉及懒加载和线程同步。
// SingletonEager.hpp #include <memory> template <typename T> class SingletonEager { public: static T& GetInstance() noexcept { return *instance_; } // 饿汉式通常不提供手动销毁,因为实例在程序启动时就已存在。 // 如果必须销毁,需格外小心静态对象销毁顺序问题。 static void DestroyInstance() { instance_.reset(); } SingletonEager(const SingletonEager&) = delete; SingletonEager& operator=(const SingletonEager&) = delete; protected: SingletonEager() = default; virtual ~SingletonEager() = default; private: static std::unique_ptr<T> instance_; }; // 静态成员初始化 - 关键!在动态初始化阶段完成。 template<typename T> std::unique_ptr<T> SingletonEager<T>::instance_(new T());使用方式与注意事项:饿汉式的使用接口与懒汉式类似,但实例在main函数开始前就已构造。最大的风险是静态初始化顺序问题(Static Initialization Order Fiasco)。如果SingletonEager<A>的实例依赖于SingletonEager<B>的实例,而编译器初始化它们的顺序是不确定的,就可能访问到未初始化的B实例。因此,饿汉式模板更适合那些构造简单、不依赖其他全局/静态对象的单例。
避坑指南:对于存在复杂依赖的单例,应优先使用懒汉式(Meyers‘或
call_once),让运行时首次调用的顺序来决定初始化顺序,这通常是安全的。如果必须用饿汉式且存在依赖,可以考虑将依赖项也封装成懒汉式单例,或者使用“构造时首次使用(Construct On First Use)”的变通方法。
4. 线程安全模板单例的进阶议题与性能考量
当我们拥有了一个通用的、线程安全的单例模板后,还需要思考一些更深层次的问题,以确保它在复杂的生产环境中依然可靠。
4.1 内存序与跨平台一致性
在C++11之前,实现双检锁是危险的,原因在于编译器和处理器会对指令进行重排序(Reordering)。对于语句instance_ = new T();,一个可能的执行顺序是:1) 分配内存,2) 将内存地址赋值给instance_(此时指针非空),3) 在分配的内存上构造T对象。如果线程A执行到步骤2后被打断,线程B看到instance_非空,便直接使用它,但此时对象还未构造,行为未定义。
C++11内存模型通过std::atomic和内存序(Memory Order)解决了这个问题。我们的std::call_once实现和局部静态变量实现,其内部都符合C++11标准定义的内存序要求(通常是std::memory_order_seq_cst,即顺序一致性,最严格但开销也最大),因此是安全的。
如果你需要自己用原子操作实现一个极致的、低开销的双检锁模板,可能会这样写:
template<typename T> class SingletonDCLP { public: static T* GetInstance() { T* tmp = instance_.load(std::memory_order_acquire); // 获取内存序 if (tmp == nullptr) { std::lock_guard<std::mutex> lock(mutex_); tmp = instance_.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new T(); instance_.store(tmp, std::memory_order_release); } } return tmp; } // ... 其他成员 private: static std::atomic<T*> instance_; static std::mutex mutex_; };这里,std::memory_order_acquire确保后续的读操作不会重排到此加载操作之前;std::memory_order_release确保之前的写操作不会重排到此存储操作之后。这一对操作构成了“同步”关系,保证了其他线程在看到非空指针时,一定能看到构造完成的对象。
重要提示:除非你是在为某个无法使用
std::call_once或局部静态变量(如某些嵌入式环境或旧标准)的环境编写代码,并且对性能有极致要求,否则不要自己手动实现双检锁。直接使用std::call_once或局部静态变量,它们更简单、更安全,并且现代编译器的优化已经非常出色,其性能开销在绝大多数应用中是可以接受的。
4.2 单例模板的继承与多重继承问题
我们的模板使用了CRTP。如果一个类已经继承了另一个非单例的基类,又想拥有单例特性,就需要使用多重继承。
class Base { /* ... */ }; class Derived : public Base, public Singleton<Derived> { friend class Singleton<Derived>; private: Derived() : Base(/* args */) { /* ... */ } };这通常能工作,但要注意:
- 钻石继承(菱形继承):如果
Base本身也间接继承了Singleton<SomeClass>,会导致重复继承Singleton的基类子对象,可能引发歧义。这种情况需要虚继承来解决,但会使设计复杂化。在实践中,应避免单例类再参与复杂的继承层次。 - 构造函数调用顺序:在
Derived的构造函数初始化列表中,需要正确初始化所有直接基类(Base和Singleton<Derived>)。Singleton的构造函数通常是默认的,问题不大。
一个更清晰的设计原则是:将单例能力视为一个“混入(Mixin)”特性,而不是核心逻辑的一部分。让业务类MyService继承Singleton<MyService>,而MyService本身保持简单的继承层次。
4.3 单例的生命周期与程序退出顺序
单例对象在程序结束时需要被析构。对于返回引用的Meyers‘ Singleton,其局部静态变量instance的析构发生在main函数结束后,静态对象析构的顺序与构造顺序相反(但不同编译单元的静态对象构造/析构顺序是未定义的!)。
这可能导致“析构顺序问题”:如果单例A的析构函数中使用了单例B,而B在A之前被析构了,那么A析构时访问的就是一个已销毁的B对象,导致未定义行为。
解决方案:
- 使用指针而非引用:像我们的
SingletonLazy返回指针,并且不提供自动销毁(或仅在程序明确结束时销毁)。让操作系统在进程退出时回收所有内存。这是一种“放任自流”的策略,前提是你的单例析构函数没有必须在程序退出前执行的逻辑(如写入文件、关闭网络连接)。对于日志系统、配置管理等单例,这通常不可接受。 - 明确的生命周期管理:设计一个
Shutdown或Release方法,在main函数返回前,按依赖关系的逆序手动调用所有单例的销毁方法。这要求你对单例间的依赖关系有清晰的认识。 - “Phoenix Singleton”模式:一种高级技巧,单例在被销毁后,如果再次被访问,能够自动重建。这通常通过一个代理类和一个静态指针来实现,在代理类的析构函数中不真正删除对象,或者在
GetInstance中检查指针并重新创建。这种模式比较复杂,且不适用于所有场景。
最佳实践建议:对于大多数应用,使用返回引用的Meyers‘ Singleton,并确保单例析构函数不依赖其他单例。如果存在依赖,考虑将依赖关系从析构函数移到独立的
Shutdown方法中,并在程序退出流程中手动控制调用顺序。
5. 实战:构建一个通用的日志管理器单例
让我们用一个具体的例子来整合上述所有知识。假设我们要实现一个线程安全的日志管理器Logger,它需要:
- 是单例的。
- 支持在初始化时指定日志文件路径。
- 线程安全地写入日志。
- 在程序结束时能正确关闭日志文件。
我们将选择SingletonLazy(基于std::call_once)模板,因为它支持带参构造和手动销毁。
// Logger.hpp #include "SingletonLazy.hpp" #include <fstream> #include <mutex> #include <string> #include <iostream> class Logger : public SingletonLazy<Logger> { friend class SingletonLazy<Logger>; public: void Log(const std::string& message) { std::lock_guard<std::mutex> lock(file_mutex_); if (log_file_.is_open()) { log_file_ << message << std::endl; } else { // 降级处理:输出到标准错误 std::cerr << "[Logger Error] File not open. Message: " << message << std::endl; } } bool SetLogFile(const std::string& filepath) { std::lock_guard<std::mutex> lock(file_mutex_); if (log_file_.is_open()) { log_file_.close(); } log_file_.open(filepath, std::ios::out | std::ios::app); return log_file_.is_open(); } // 提供一个显式的清理方法,而不是依赖析构函数(因为析构顺序不确定) static void Shutdown() { auto* instance = GetInstance(); if (instance) { std::lock_guard<std::mutex> lock(instance->file_mutex_); if (instance->log_file_.is_open()) { instance->log_file_ << "Logger shutting down." << std::endl; instance->log_file_.close(); } } // 可以选择是否销毁单例实例 // DestroyInstance(); } private: // 私有构造函数,可以接受初始文件路径 Logger(const std::string& initial_path = "default.log") { if (!initial_path.empty()) { log_file_.open(initial_path, std::ios::out | std::ios::app); if (!log_file_.is_open()) { std::cerr << "Failed to open log file: " << initial_path << std::endl; } } } ~Logger() { // 注意:析构函数在静态对象销毁时调用,此时可能其他静态对象(包括其他单例)已失效。 // 因此,重要的清理工作应在Shutdown()中完成。 if (log_file_.is_open()) { // 这里只做最简单的关闭,不写入任何东西,因为依赖可能已失效。 log_file_.close(); } } std::ofstream log_file_; std::mutex file_mutex_; // 保护文件流操作 }; // Logger.cpp - 需要单独定义静态成员 // template<> std::unique_ptr<Logger> SingletonLazy<Logger>::instance_ = nullptr; // template<> std::once_flag SingletonLazy<Logger>::once_flag_; // 注意:模板静态成员的定义通常在头文件中完成(通过inline变量C++17后更好), // 或者在一个单独的.ipp文件中包含。为了简化,我们在SingletonLazy.hpp中使用了inline(C++17)。 // 如果编译器不支持C++17 inline变量,则需要在.cpp文件中进行特化定义,如上所示。使用示例:
// main.cpp #include "Logger.hpp" #include <thread> #include <vector> void thread_func(int id) { for (int i = 0; i < 5; ++i) { Logger::GetInstance()->Log("Thread " + std::to_string(id) + ": message " + std::to_string(i)); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } int main() { // 初始化日志,指定文件路径 Logger::GetInstance("application.log"); std::vector<std::thread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(thread_func, i); } for (auto& t : threads) { t.join(); } // 程序结束前,显式关闭日志管理器 Logger::Shutdown(); // 如果需要,也可以销毁单例实例 // Logger::DestroyInstance(); return 0; }这个实战案例的关键点:
- 模板的应用:
Logger类通过继承SingletonLazy<Logger>,轻松获得了线程安全的、支持带参构造的单例能力,无需自己管理once_flag和指针。 - 线程安全的日志写入:在
Log方法内部使用了互斥锁file_mutex_,确保多线程同时写日志不会造成数据交错或文件操作冲突。 - 生命周期的显式管理:提供了
Shutdown()静态方法,而不是完全依赖析构函数。这避免了在静态析构阶段可能出现的依赖问题。在main函数退出前显式调用Shutdown(),确保日志文件被正确刷新和关闭。 - 资源获取即初始化(RAII):即使不调用
Shutdown(),在Logger析构函数中也会尝试关闭文件。这是一种兜底策略,但核心的、有依赖的清理逻辑放在Shutdown()中更安全。 - 错误处理与降级:在日志文件打开失败时,降级到
std::cerr输出,提高了鲁棒性。
通过这个例子,你可以看到,一个精心设计的模板单例基类,如何让业务类的开发变得专注和简洁,同时保证了核心的线程安全和资源管理需求。
