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

在多线程环境下,单例模式的实现往往需要考虑并发访问导致的竞争条件。下面给出一种既简单又高效、兼顾C++11标准下原子操作与懒初始化的实现方式。

1. 懒初始化 + C++11 的 std::call_once

C++11 引入了 std::call_oncestd::once_flag,这两个工具可以保证函数体只执行一次,且线程安全。示例代码如下:

#include <iostream>
#include <mutex>

class Singleton
{
public:
    // 获取单例实例的静态接口
    static Singleton& instance()
    {
        std::call_once(initFlag, [](){
            // 这里会在多线程环境下只执行一次
            ptr = new Singleton();
        });
        return *ptr;
    }

    // 防止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 示例业务方法
    void doSomething()
    {
        std::cout << "Doing something with address: " << this << std::endl;
    }

private:
    Singleton()  { std::cout << "Singleton constructor\n"; }
    ~Singleton() { std::cout << "Singleton destructor\n"; }

    static Singleton* ptr;
    static std::once_flag initFlag;
};

// 静态成员定义
Singleton* Singleton::ptr = nullptr;
std::once_flag Singleton::initFlag;

优点

  1. 线程安全std::call_once 内部使用原子操作,避免了传统的 mutex + double-checked locking 的复杂性。
  2. 延迟初始化:实例化仅在第一次调用 instance() 时发生,节省启动时资源。
  3. 性能:第一次初始化后,后续获取实例仅需一次原子检查,无需加锁。

2. 静态局部变量(Meyers Singleton)

C++11 规范保证了局部静态变量的初始化是线程安全的。因此可以采用更简洁的方式:

class Singleton
{
public:
    static Singleton& instance()
    {
        static Singleton instance; // C++11 线程安全
        return instance;
    }

    // 其余与上面相同
    ...
};

注意

  • 这种方式在程序退出时,析构函数会被调用(除非程序异常终止)。
  • 若单例对象管理资源需要在程序终止前显式释放,可结合 std::shared_ptr 或手动销毁。

3. 结合智能指针的懒加载

若单例需要在销毁前做特定操作(如写日志、关闭网络等),可以用 std::shared_ptr 与自定义删除器:

class Singleton
{
public:
    static std::shared_ptr <Singleton> instance()
    {
        std::call_once(initFlag, [](){
            ptr = std::shared_ptr <Singleton>(new Singleton(),
                     [](Singleton* p){ /* 自定义清理逻辑 */ delete p; });
        });
        return ptr;
    }

    ...
private:
    static std::shared_ptr <Singleton> ptr;
    static std::once_flag initFlag;
};

4. 性能考虑

  • 单例实例大小:保持单例对象体积小,避免不必要的成员。
  • 访问方式:若只读访问,考虑把成员设为 constexprconst,减少运行时开销。
  • 构造成本:若构造非常昂贵,可使用异步预热技术(线程池提前初始化)。

5. 典型使用场景

场景 推荐实现
需要延迟初始化,且只读 Meyers Singleton
需要在多线程启动时完成复杂资源分配 call_once + once_flag
需要在程序结束时做自定义清理 shared_ptr + 自定义删除器

6. 常见陷阱

  1. 非原子操作:手动实现 double-checked locking 时需使用 std::atomic,否则会出现指令重排导致的访问错误。
  2. 对象销毁顺序:全局静态对象在程序结束时按逆序销毁,若单例引用了其他静态对象,可能导致悬空引用。
  3. 异常安全:若构造函数抛异常,std::call_once 会重新尝试初始化;若使用静态局部变量,异常会导致后续访问仍然失败。

小结

在 C++11 及以后版本,最推荐的线程安全单例实现是使用 std::call_oncestd::once_flag,或更简洁的静态局部变量方式。两种实现均已被标准库证明为线程安全,且代码简洁易维护。根据业务需求选择合适的实现即可。

发表评论