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

在多线程环境下,单例模式的实现往往会成为安全性和性能的双重挑战。下面我们从两个角度来探讨在C++17中实现线程安全单例的几种常见方案,并对比它们的优缺点。

1. Meyers’ Singleton(局部静态对象)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11以后初始化是线程安全的
        return instance;
    }
    // 删除拷贝构造和赋值操作,防止外部复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};

优点

  • 代码简洁,几乎不需要额外的同步机制。
  • 对象生命周期由编译器管理,避免手工删除。
  • 适用于大多数需求,尤其是在懒加载(lazy loading)时。

缺点

  • 对象在程序结束时才会析构,若需要提前释放资源,需自行手动销毁或使用 std::unique_ptr 包装。
  • 仅适用于C++11及以后编译器,旧编译器不支持。

2. 双重检查锁(Double-Checked Locking)

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

    // 同上删除拷贝构造与赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;

    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;

优点

  • 通过原子操作减少了锁的粒度,仅在第一次实例化时进入互斥。

缺点

  • 代码相对复杂,需要 careful memory ordering,易出错。
  • 仍然需要手动删除实例(如在 atexit 注册销毁函数)以避免资源泄漏。

3. 显式销毁 + std::unique_ptr

class Singleton {
public:
    static Singleton& instance() {
        static std::once_flag flag;
        static std::unique_ptr <Singleton> ptr;
        std::call_once(flag, [](){
            ptr.reset(new Singleton());
            std::atexit(&Singleton::destroy);
        });
        return *ptr;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;

    static void destroy() {
        ptr.reset();          // 释放资源
    }
};

优点

  • std::call_once 保证一次性初始化,线程安全。
  • 使用 std::unique_ptr 自动管理内存,避免泄漏。
  • 通过 atexit 可确保在程序结束时显式销毁。

缺点

  • 需要额外的 ptr 声明在类外,略显繁琐。
  • 在多次 instance() 调用后,析构仍在程序结束时,若需要提前释放,需额外操作。

4. 线程局部存储(TLS)方式

如果每个线程需要独立的单例实例,可以使用线程局部存储:

class ThreadSingleton {
public:
    static ThreadSingleton& instance() {
        thread_local ThreadSingleton instance;   // 每个线程创建自己的实例
        return instance;
    }

    ThreadSingleton(const ThreadSingleton&) = delete;
    ThreadSingleton& operator=(const ThreadSingleton&) = delete;

private:
    ThreadSingleton() = default;
    ~ThreadSingleton() = default;
};

优点

  • 每个线程都拥有自己的实例,避免了跨线程共享问题。
  • 简单易懂,使用 thread_local 关键字即可。

缺点

  • 不是传统意义上的单例(多实例),仅适用于特定需求。
  • 对于需要跨线程共享资源的情况不适用。

5. 评估与选择

方案 线程安全性 资源释放 适用场景 代码复杂度
Meyers’ 程序结束 需要懒加载、资源不需要提前释放 简单
双重检查锁 需手动释放 性能敏感、需要早期释放 较复杂
std::call_once + unique_ptr 自动释放 需要显式销毁 中等
TLS 每线程自行销毁 线程局部单例 简单
  • 如果你使用的是C++11及以后且不需要提前销毁资源,推荐使用 Meyers’ Singleton,最简单、最稳健。
  • 如果你在资源释放时有特殊需求(如早期关闭数据库连接),可考虑 std::call_once + unique_ptr双重检查锁
  • 如果每个线程需要独立实例,使用 TLS

6. 小结

C++17 提供了丰富的原子、锁以及线程局部存储机制,使得实现线程安全单例变得既灵活又高效。最重要的是,根据业务需求选择最合适的实现方案,而不是盲目追求最“优雅”的代码。通过对比上述方案,你可以在安全性、性能与可维护性之间找到最佳平衡点。

发表评论