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

在多线程环境下实现一个单例对象时,最常见的难点是保证对象只被创建一次且在所有线程之间安全可见。下面以C++17为例,演示几种常用且线程安全的实现方式,并对其优缺点进行简要讨论。

1. 本地静态变量(Meyers单例)

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

    // 其他公共接口
    void do_something() { /* ... */ }

private:
    Singleton()  = default;            // 私有构造
    ~Singleton() = default;            // 私有析构
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

原理

在 C++11 之后,编译器保证对局部静态对象的初始化是线程安全的。首次访问 instance() 时,inst 以原子方式完成构造,随后所有线程都能安全访问同一实例。

优点

  • 代码简洁,几乎不需要额外的同步机制。
  • 延迟初始化,直到真正需要实例时才构造。

缺点

  • 无法控制实例的销毁时机(在程序退出时自动销毁)。
  • 对于需要按需销毁或重置的单例场景不够灵活。

2. std::call_oncestd::unique_ptr

#include <memory>
#include <mutex>

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

    void do_something() { /* ... */ }

private:
    Singleton()  = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

原理

std::call_once 只会让第一次调用时执行给定的 lambda,其余线程会等待直到初始化完成。std::unique_ptr 管理实例生命周期。

优点

  • 可在需要时显式销毁单例(如 instancePtr.reset()),满足某些应用需求。
  • std::call_once 的语义更清晰,易于理解。

缺点

  • 代码略显冗长,需手动维护静态成员。

3. 原子指针 + 双重检查锁(DCL)

#include <atomic>
#include <mutex>

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instancePtr.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instancePtr.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instancePtr.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

    void do_something() { /* ... */ }

private:
    Singleton()  = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::atomic<Singleton*> instancePtr;
    static std::mutex mtx;
};

std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;

原理

第一次检测到 instancePtrnullptr 后,线程会尝试获取互斥锁并再次检查,确保只有一个线程执行实例化。std::memory_order_acquire/release 保证可见性。

优点

  • 适用于需要在不同平台上手动控制同步细节的老旧代码。
  • 可在 C++11 之前使用(只需自行实现原子与锁)。

缺点

  • 代码更易出错,必须正确使用内存序。
  • 过度同步会导致性能瓶颈,尤其在实例已创建后每次访问仍需检查 instancePtr

4. 对比与选择

方法 线程安全性 是否延迟初始化 销毁控制 代码复杂度
本地静态变量 自动
call_once/unique_ptr 手动
双重检查锁 手动
传统单例(无同步)
  • 最推荐:若项目已使用 C++11 或更高版本,首选 本地静态变量。其实现最简洁,且线程安全保证由标准提供。
  • 需要销毁控制:使用 call_once + unique_ptrstd::shared_ptr(若需要共享所有权)来显式管理单例生命周期。
  • 老项目或特殊需求:若项目对同步细节有特殊需求(如需要自定义内存序),可考虑 双重检查锁

5. 小结

在多线程环境下实现单例模式时,关键是保证“只创建一次”和“所有线程可见”。自 C++11 起,标准库提供了足够成熟的工具(std::call_once、局部静态变量)来实现这一点,开发者不必再手动写复杂的锁代码。只有在极少数情况下(如需要自定义销毁时机、支持共享所有权或兼容旧编译器)才需要使用更复杂的方案。选择合适的实现方式,既能保证线程安全,又能保持代码简洁与易维护。

发表评论