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

在多线程环境下,单例(Singleton)模式需要保证:

  1. 只创建一次实例;
  2. 对所有线程可见;
  3. 初始化过程是原子性的。

下面给出几种常用实现方式,并说明其优缺点。

1. 采用Meyers Singleton(局部静态变量)

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& instance() {
        static ThreadSafeSingleton inst;   // C++11之后保证线程安全
        return inst;
    }

    // 禁止拷贝构造和赋值
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

    void doSomething() {
        // 业务逻辑
    }

private:
    ThreadSafeSingleton() { /* 可能的初始化 */ }
};

优点

  • 代码最简洁,几乎不需要手动同步。
  • 对象在第一次调用instance()时才创建,支持懒加载。
  • C++标准保证局部静态变量的初始化是线程安全的(C++11及以后)。

缺点

  • 如果实例构造失败,后续调用会重复尝试,导致异常抛出。
  • 在某些编译器/标准库实现中,初始化过程中出现竞态可能导致性能下降(虽然理论上是安全的)。

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

#include <mutex>

class DCLSingleton {
public:
    static DCLSingleton* getInstance() {
        if (!ptr) {                                 // 第一次检查(无锁)
            std::lock_guard<std::mutex> lock(mtx);
            if (!ptr) {                             // 第二次检查(加锁)
                ptr = new DCLSingleton();
            }
        }
        return ptr;
    }

private:
    DCLSingleton() {}
    ~DCLSingleton() {}

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

    static DCLSingleton* ptr;
    static std::mutex mtx;
};

DCLSingleton* DCLSingleton::ptr = nullptr;
std::mutex DCLSingleton::mtx;

优点

  • 对于多线程并发请求,只有第一次实例化时才加锁,后续请求几乎无锁。

缺点

  • 需要显式地使用mutex,容易出现忘记加锁或误删锁的错误。
  • 在旧标准(C++03)下,因内存可见性问题会出现“指针可见但未初始化”的错误。
  • 现代C++下推荐使用Meyers Singleton,更安全简洁。

3. 静态局部对象 + std::call_once(更显式控制)

#include <mutex>

class OnceSingleton {
public:
    static OnceSingleton& getInstance() {
        std::call_once(initFlag, [](){
            instancePtr = new OnceSingleton();
        });
        return *instancePtr;
    }

private:
    OnceSingleton() {}
    ~OnceSingleton() {}

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

    static OnceSingleton* instancePtr;
    static std::once_flag initFlag;
};

OnceSingleton* OnceSingleton::instancePtr = nullptr;
std::once_flag OnceSingleton::initFlag;

优点

  • 明确展示初始化逻辑,避免因局部静态对象的隐式实现而产生的疑惑。
  • std::call_once保证只调用一次,即使有异常也能保证安全。

缺点

  • 需要手动管理实例指针,可能导致手动删除或内存泄漏。
  • 相对Meyers Singleton来说实现略显冗长。

4. 使用std::unique_ptr + std::atomic(现代化写法)

#include <memory>
#include <atomic>
#include <mutex>

class AtomSingleton {
public:
    static AtomSingleton& instance() {
        AtomSingleton* 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 AtomSingleton();
                instancePtr.store(tmp, std::memory_order_release);
            }
        }
        return *tmp;
    }

private:
    AtomSingleton() {}
    ~AtomSingleton() {}

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

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

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

优点

  • 使用atomicmemory_order可显式控制可见性,适用于需要在多进程或低级别优化的场景。

缺点

  • 代码复杂度较高,易出错;通常不需要这么底层的控制。

结论

  • 对大多数应用,Meyers Singleton(局部静态变量)是最推荐的实现方式。
  • 如果你需要在构造失败时进行重试或需要更细粒度的异常处理,考虑std::call_once
  • 只在特殊性能或兼容性需求下才使用双重检查锁原子指针实现。

通过上述几种实现,你可以根据项目需求、编译器版本和线程安全要求,选择最合适的单例模式实现。

发表评论