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

在多线程环境下,确保单例对象只被创建一次,并且对所有线程可见,通常被称为“线程安全单例”。下面介绍几种常见实现方式,并讨论它们的优缺点。


1. C++11 及以后:使用 std::call_oncestd::once_flag

C++11 引入了线程同步原语 std::call_oncestd::once_flag,可以保证某个函数只被调用一次。实现单例的典型代码如下:

#include <mutex>

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

    // 禁止拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

// 静态成员定义
std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

优点

  • 简洁:不需要手写双重检查锁定(Double-Checked Locking)等复杂代码。
  • 性能call_once 只在第一次调用时产生锁,随后访问无需加锁。
  • 可移植:标准库实现保证在所有支持 C++11 的编译器上都能工作。

缺点

  • 销毁顺序:如果使用 unique_ptr,在程序结束时单例会被销毁;若需要在特定顺序销毁,可能需要手动管理。

2. C++11:局部静态变量(Meyers 单例)

从 C++11 开始,局部静态变量的初始化是线程安全的。实现方式最简洁:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 线程安全初始化
        return instance;
    }

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

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

优点

  • 代码最短:无须任何同步原语,直接利用语言特性。
  • 销毁顺序:局部静态在程序结束时会自动销毁,顺序由编译器决定,符合 C++ 的销毁顺序规则。

缺点

  • 懒加载:如果单例在程序启动前就被使用,可能导致延迟。
  • 异常安全:如果构造函数抛出异常,后续 getInstance() 调用会再次尝试构造,直到成功。

3. 传统实现:双重检查锁定(Double-Checked Locking)

在 C++11 之前,常见的做法是使用互斥锁和双重检查。示例代码:

#include <mutex>

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;

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

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

优点

  • 可在 C++98/03 环境中使用:不依赖 C++11 特性。

缺点

  • 难以保证正确性:在缺乏强内存模型支持时,可能导致数据竞争。
  • 性能成本:每次访问都需要检查指针,虽然锁是可读锁,但仍有一定开销。

4. 现代化方案:使用 std::shared_ptr + std::atomic

如果单例需要在多个线程之间共享并可能被析构(如插件系统),可以使用原子操作和 shared_ptr

#include <atomic>
#include <memory>

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

    // ...

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

    static std::atomic<std::shared_ptr<Singleton>> instance;
    static std::mutex mutex_;
};

std::atomic<std::shared_ptr<Singleton>> Singleton::instance{nullptr};
std::mutex Singleton::mutex_;

优点

  • 可析构:当所有引用消失时,单例会自动析构,适用于需要动态资源管理的场景。
  • 线程安全:使用 std::atomicstd::memory_order 保证可见性。

缺点

  • 实现更复杂:需要理解原子操作与内存序。
  • 性能开销:虽然在没有竞争时几乎无锁,但在高并发下仍有一定代价。

5. 选择建议

场景 推荐实现
只需要单例且在 C++11 及以后 std::call_once + unique_ptr 或局部静态(Meyers)
需要在旧编译器 (C++03) 上编译 双重检查锁定 + 互斥锁
单例需要可析构、可被多线程共享 std::shared_ptr + std::atomic
代码简洁且不关心销毁顺序 局部静态(Meyers)

6. 小结

线程安全单例是 C++ 并发编程中的经典问题。随着标准库的不断完善,C++11 之后的实现变得极为简洁且高效。开发者应根据项目需求(编译器支持、资源生命周期、性能要求)选择最合适的实现方式,并遵循现代 C++ 的最佳实践,避免不必要的手写锁与指针操作,以提升代码的可读性和安全性。

发表评论