如何在 C++ 中实现线程安全的单例模式

在多线程环境下,单例模式的实现往往面临“线程安全”与“性能”两大挑战。本文将从两种常见实现方式:懒汉式(Lazy)和饿汉式(Eager),分别讨论其线程安全的实现细节,并给出完整可编译的示例代码,帮助你在实际项目中快速落地。

1. 懒汉式(Lazy)——按需创建

懒汉式单例的核心是按需创建实例,初始状态下不占用资源。最常见的线程安全实现是使用 std::call_once 配合 std::once_flag

#include <iostream>
#include <mutex>

class LazySingleton {
public:
    static LazySingleton& instance() {
        std::call_once(initFlag, []() {
            instancePtr.reset(new LazySingleton);
        });
        return *instancePtr;
    }

    void sayHello() const { std::cout << "Hello from LazySingleton!\n"; }

private:
    LazySingleton() { std::cout << "LazySingleton constructed\n"; }
    ~LazySingleton() = default;
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

    static std::unique_ptr <LazySingleton> instancePtr;
    static std::once_flag initFlag;
};

std::unique_ptr <LazySingleton> LazySingleton::instancePtr;
std::once_flag LazySingleton::initFlag;

1.1 关键点说明

关键点 说明
std::once_flag 只允许一次执行,确保初始化仅发生一次
std::call_once 线程安全地调用一次 lambda 以创建实例
unique_ptr 自动管理单例生命周期,避免手动 delete
拷贝/赋值禁用 防止外部复制导致多实例

该实现优点是线程安全、延迟加载且无需额外锁,性能接近单线程初始化。

2. 饿汉式(Eager)——静态初始化

饿汉式在程序启动时就完成实例化,天然线程安全(因为 C++11 之后,函数内部静态对象按首次访问时初始化,且初始化是线程安全的)。实现非常简洁:

class EagerSingleton {
public:
    static EagerSingleton& instance() {
        static EagerSingleton instance;
        return instance;
    }

    void sayHello() const { std::cout << "Hello from EagerSingleton!\n"; }

private:
    EagerSingleton() { std::cout << "EagerSingleton constructed\n"; }
    ~EagerSingleton() = default;
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;
};

2.1 何时选用饿汉式?

  • 不需要延迟:如果单例本身占用资源不多,或者应用启动时就会使用到,饿汉式更简洁。
  • 构造开销小:如果构造函数复杂,且启动期间不会受影响,饿汉式更安全。

3. 结合 C++20 的 constevalconstinit

C++20 引入 constevalconstinit,可以进一步保证单例在编译期或常量初始化时就完成:

class CompileTimeSingleton {
public:
    static constinit CompileTimeSingleton& instance() {
        static CompileTimeSingleton inst;
        return inst;
    }
    // ...
private:
    CompileTimeSingleton() {}
    // ...
};

constinit 保证 instance 的静态存储对象在编译期就完成初始化,避免运行时延迟。

4. 常见错误与陷阱

  1. 双重检查锁(Double-Check Locking)
    旧版 C++ 实现往往使用 if (!ptr) { lock(); if (!ptr) ptr = new T; }。若不使用 volatile 或内存屏障,可能导致未初始化对象被使用。std::call_once 是安全且简洁的替代方案。

  2. 全局静态破坏顺序
    单例销毁顺序不确定,若在 main 结束后仍使用单例,可能出现野指针。建议单例使用 std::unique_ptrstd::call_once,或让单例永不过期(如使用 constinit)。

  3. 跨线程的静态成员
    在多线程环境中,任何访问单例的函数都必须保证是线程安全的。若单例内部持有可变状态,需使用 std::mutexstd::atomic 进行同步。

5. 小结

  • 懒汉式:使用 std::call_oncestd::atomic 以确保一次性初始化,适合延迟加载场景。
  • 饿汉式:利用 C++11 对局部静态的线程安全初始化特性,代码最简洁,适合无延迟需求。
  • C++20constinitconsteval 可进一步提升编译期安全性。

根据实际需求(资源占用、延迟、线程安全保障程度)选择合适的实现方式即可。祝你在 C++ 项目中顺利使用单例模式,构建稳健且高效的代码架构!

发表评论