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

在多线程环境下,确保单例对象只被创建一次且在任何线程中都能安全访问,是一个常见但细节繁琐的任务。下面将从 C++11 起支持的标准特性出发,介绍几种既安全又高效的实现方式,并讨论其优缺点。


1. 经典懒汉式 + std::call_once

#include <mutex>

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

    // 其他成员函数...
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::once_flag flag_;
    static Singleton* ptr_;
};

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

优点

  • 延迟初始化:真正需要时才创建实例。
  • 线程安全std::call_once 保证即使多个线程同时调用 instance(),只会有一次调用其内部 lambda。
  • 无锁std::call_once 在内部使用了高效的硬件原语。

缺点

  • 对象在程序结束时不一定被析构(单例持久化)。如果需要在退出时清理,可在 atexit() 注册析构函数或使用 std::unique_ptr 并配合 std::atexit

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

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 之后保证线程安全
        return instance;
    }
    // ...
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 简洁:只需一句 static 声明。
  • 线程安全:C++11 起编译器保证局部静态变量的初始化是线程安全的。
  • 自动析构:程序结束时 instance 会被自动销毁。

缺点

  • 初始化顺序未定义:如果在构造函数中使用了其他全局对象,可能导致“静态初始化顺序问题”。
  • 销毁时机不可控:若在 main() 结束前访问,可能已被销毁导致悬垂指针。

3. 带有锁的双检锁(Double-Checked Locking)

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        Singleton* tmp = instance_;
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_;
            if (!tmp) {
                tmp = new Singleton();
                instance_ = tmp;
            }
        }
        return *tmp;
    }
    // ...
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    static std::mutex mutex_;
};

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

优点

  • 性能:第一次实例化后后续访问不需要加锁。
  • 延迟创建:与 call_once 类似。

缺点

  • 易错:必须保证 instance_ 的写操作对所有线程可见,使用 std::atomic<Singleton*>volatile。否则可能出现指令重排导致的未初始化对象泄漏。
  • 实现复杂:相比前两种实现,代码更繁琐。

4. C++17 的 inline 变量 + std::once_flag

如果你使用 C++17 或更高版本,可以将 std::once_flag 和指针声明为 inline,进一步简化。

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []{ ptr_ = new Singleton(); });
        return *ptr_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    inline static std::once_flag flag_;
    inline static Singleton* ptr_ = nullptr;
};

优点

  • 声明与定义合一:不需要在 .cpp 文件中再次定义静态成员。
  • 保持线程安全:同 call_once 的实现。

5. 什么时候选哪种?

场景 推荐实现 说明
需要最小代码量 Meyers’ Singleton 简洁、自动析构
需要显式销毁或定时释放 call_once + std::unique_ptr 手动控制生命周期
需要在全局初始化前使用 call_once + 静态指针 避免静态初始化顺序问题
性能极限要求(后续访问不加锁) 双检锁(但需注意原子) 复杂度最高,易错

6. 小结

  • C++11 以后,局部静态变量的初始化已变得线程安全,Meyers’ Singleton 成为最简洁的选择。
  • 对于更细粒度的控制,std::call_once 提供了安全且高效的“一次性初始化”机制。
  • 双检锁虽然理论上能减少锁开销,但实现细节繁琐,除非确有性能瓶颈且经验足够丰富,否则不建议使用。

通过合理选择实现方式,可在多线程 C++ 项目中轻松使用单例模式,而不必担心并发安全问题。祝编码愉快!

发表评论