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

在多线程环境下,单例模式的实现需要特别注意线程安全性。下面介绍几种常用的实现方式,并讨论它们的优缺点。


1. 经典Meyers Singleton(局部静态变量)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 起线程安全
        return instance;
    }
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 简洁:只需要一行代码即可完成。
  • 延迟初始化:真正需要时才创建实例。
  • 线程安全:自 C++11 起,局部静态变量的初始化已保证原子性,避免了双重检查锁的复杂性。

缺点

  • 不可在类析构前显式销毁:若需要在程序结束前手动销毁对象,可使用std::unique_ptr配合std::atexit实现。
  • 调试困难:如果构造函数抛异常,可能导致后续调用失败。

2. 双重检查锁(DCL)

class Singleton {
public:
    static Singleton* instance() {
        if (!instance_) {
            std::lock_guard<std::mutex> lock(mtx_);
            if (!instance_) {
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
    static void destroy() {
        std::lock_guard<std::mutex> lock(mtx_);
        delete instance_;
        instance_ = nullptr;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    static Singleton* instance_;
    static std::mutex mtx_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;

优点

  • 适用于 C++11 前的编译器。
  • 能在程序结束前手动销毁单例。

缺点

  • 实现复杂:需要正确使用volatile(或std::atomic)和双重检查。
  • 潜在的优化缺陷:编译器可能重排指令,导致可见性问题。
  • 性能:第一次访问时会进行两次检查和一次锁操作,略低于Meyers实现。

3. std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){ instance_ = new Singleton(); });
        return *instance_;
    }
private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

优点

  • 线程安全std::call_once确保初始化只执行一次。
  • 清晰易懂:不需要手动加锁。
  • 兼容性好:适用于所有 C++11 及之后的标准。

缺点

  • 仍需手动销毁(如果想释放资源)。
  • 与Meyers Singleton相比,代码略显冗长。

4. 静态函数对象(Lambda)

class Singleton {
public:
    static Singleton& instance() {
        static auto ptr = []() -> Singleton* {
            return new Singleton();
        }();
        return *ptr;
    }
private:
    Singleton() = default;
};

优点

  • 利用Lambda延迟实例化,兼顾线程安全。
  • 可通过返回指针实现懒销毁。

缺点

  • 仍为C++11实现,代码略显复杂。

5. 线程安全的懒加载+智能指针

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(flag_, [](){ ptr_ = std::make_shared <Singleton>(); });
        return ptr_;
    }
private:
    Singleton() = default;
    static std::shared_ptr <Singleton> ptr_;
    static std::once_flag flag_;
};

std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;

优点

  • 自动管理生命周期,避免手动delete。
  • 可多线程共享同一实例。

缺点

  • shared_ptr会引入一次引用计数的开销。

何时选哪种实现?

场景 推荐实现 理由
只需要单例,且C++11+ Meyers Singleton 简洁、线程安全、延迟
需要手动销毁或在C++11之前编译 双重检查锁 兼容性
需要显式一次性初始化控制 std::call_once 语义清晰、线程安全
需要共享计数、可能在多个线程释放 shared_ptr + call_once 自动内存管理

常见陷阱

  1. 静态对象销毁顺序

    • 如果多个单例相互依赖,可能出现“static deinitialization order fiasco”。
    • 解决方案:使用 std::call_onceMeyers Singleton,避免在析构中访问其他静态对象。
  2. 抛异常的构造函数

    • Meyers Singleton 在构造抛异常后,后续再次调用会再次尝试初始化,可能导致重复异常。
    • 可使用 std::unique_ptr 包装并在异常时清理。
  3. 多进程环境

    • 单例只能在进程内唯一。若跨进程需要使用共享内存或文件锁。

小结

C++11以后,Meyers Singletonstd::call_once 已经可以轻松实现线程安全的单例,开发者可以根据项目需求选择最适合的实现方式。关键是保证延迟初始化一次性执行以及线程安全,并注意对象销毁时机和依赖关系。祝编码愉快!

发表评论