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

单例模式是一种常见的设计模式,用来保证一个类在整个程序运行期间只有一个实例。随着多线程编程的普及,线程安全的单例实现成为了一个热点话题。本文从几个常见的实现方式出发,分析它们的优缺点,并给出一种高效、延迟加载、可维护的实现方案。

1. 传统双检锁(Double‑Check Locking)

class Singleton {
public:
    static Singleton& getInstance() {
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {
                instance_ = new Singleton();
            }
        }
        return *instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton* instance_;
    static std::mutex mutex_;
};
  • 优点:延迟加载,首次访问时才创建实例。
  • 缺点:需要手动管理内存,容易导致内存泄漏;在 C++11 之前,new 的顺序和可见性问题导致双检锁不安全。
  • 结论:仅在极端性能要求且对线程安全有特殊需求时才考虑使用。

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

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 线程安全的局部静态
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  • 优点:实现简洁,编译器负责线程安全初始化。C++11 标准保证局部静态初始化是线程安全的。
  • 缺点:无法手动销毁实例,可能导致资源在程序退出前无法释放。
  • 结论:在大多数场景下,这是最推荐的实现方式。

3. std::call_once + std::once_flag

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag_, [](){ instance_ = new Singleton(); });
        return *instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton* instance_;
    static std::once_flag initFlag_;
};
  • 优点:可在多线程环境下精确控制一次性初始化,且可以在程序结束时手动销毁。
  • 缺点:实现略显冗长,仍需手动管理内存。
  • 结论:适用于需要在程序运行期间显式销毁单例的场景。

4. C++17 的 std::shared_ptr + std::make_shared

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        static std::shared_ptr <Singleton> instance = std::make_shared<Singleton>();
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  • 优点:自动管理内存,允许多处引用共享单例。
  • 缺点:如果某处忘记释放引用,可能导致单例不被销毁。
  • 结论:适用于需要共享所有权的复杂系统。

5. 推荐实现方案(C++20+):std::once_flag + std::unique_ptr

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag_, []() {
            instance_ = std::unique_ptr <Singleton>(new Singleton());
        });
        return *instance_;
    }
    // 为了可销毁,提供销毁函数
    static void destroy() {
        std::call_once(destroyFlag_, []() {
            instance_.reset();
        });
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static std::unique_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
    static std::once_flag destroyFlag_;
};
  • 特点
    • std::call_once 保证初始化仅执行一次,线程安全。
    • std::unique_ptr 自动释放资源,避免泄漏。
    • 可手动销毁,满足资源释放时机控制。
    • 兼容 C++11 及以后版本。

6. 小结

  1. 最简单:Meyers Singleton(局部静态变量)。
  2. 需要手动销毁std::call_once + std::unique_ptrstd::shared_ptr
  3. 高性能:双检锁在 C++11 之后已不再安全,除非对平台细节非常了解,否则不建议使用。

在实际项目中,优先考虑可读性、易维护性与线程安全。Meyers Singleton 由于其简洁与标准化,通常是首选方案。若有特殊需求,如在程序退出前必须释放资源,可结合 std::call_once 与智能指针进行改造。

祝你编码愉快!

发表评论