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

单例模式的核心在于确保一个类只有一个实例,并提供全局访问点。随着 C++11 及其后的标准引入了多线程支持,传统的单例实现方式(如饿汉式、懒汉式)需要额外的同步机制来保证线程安全。下面介绍几种常见且高效的实现方法,并对比其优缺点。

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

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 guarantees thread-safe initialization
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 优点:实现最简洁,编译器负责线程安全。无显式锁,性能优良。
  • 缺点:如果你想在程序结束前显式销毁单例,需要自定义 std::atexit 或使用 std::unique_ptr 包装。

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

class Singleton {
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            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 mtx;
};

std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
  • 优点:延迟初始化,且线程安全。
  • 缺点:实现相对复杂,易出错。现代编译器和标准库的实现已经足够优雅,通常不需要手动实现。

3. 显式销毁的懒汉式(使用 std::unique_ptr

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;
std::once_flag Singleton::initFlag;
  • 优点:支持在程序退出时显式销毁,避免懒汉式带来的“野指针”问题。
  • 缺点:需要 `#include `、“,但实现仍然较为简洁。

4. 静态局部对象与 std::shared_ptr(可定制生命周期)

如果你想让单例对象在多处持有引用,可以结合 std::shared_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        static std::shared_ptr <Singleton> ptr(new Singleton, [](Singleton* p){ delete p; });
        return ptr;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 优点:可以通过引用计数控制对象的生命周期,适用于需要在多线程环境中共享实例的场景。
  • 缺点:若不小心产生循环引用,可能导致资源泄漏。

5. 线程安全的双向单例(双重初始化 + 读写锁)

在高并发读多写少的场景下,读写锁可以提升性能:

class Singleton {
public:
    static Singleton* getInstance() {
        std::shared_lock<std::shared_mutex> rlock(rwMutex);
        if (!instance) {
            rlock.unlock();
            std::unique_lock<std::shared_mutex> wlock(rwMutex);
            if (!instance) {
                instance = new Singleton;
            }
            rlock.lock();
        }
        return instance;
    }

private:
    static Singleton* instance;
    static std::shared_mutex rwMutex;
};

Singleton* Singleton::instance = nullptr;
std::shared_mutex Singleton::rwMutex;
  • 优点:多线程读操作不阻塞,写操作仍然保证安全。
  • 缺点:实现较为复杂,且在实例化后不再需要写锁,可能导致不必要的锁开销。

何时选择哪种实现?

实现方式 适用场景 主要优势 主要缺点
局部静态变量 简单、无销毁需求 代码最简洁,编译器自动线程安全 不能显式销毁
std::call_once + unique_ptr 需要显式销毁 线程安全,资源可被清理 需要额外头文件
双重检查锁 兼容旧编译器 延迟初始化 实现复杂,潜在错误
shared_ptr 需要共享实例 自动管理生命周期 可能出现循环引用
读写锁 读多写少 高并发读 复杂,写锁开销

在大多数现代 C++ 项目中,局部静态变量(Meyers Singleton)已成为默认首选,因为它实现最简洁、性能最优,并且在 C++11 之后已被标准保证线程安全。只有在特殊需求(如显式销毁、共享实例或读写锁优化)下才考虑其他实现。

小结

  • 线程安全:C++11 引入的局部静态变量或 std::call_once 可轻松实现。
  • 延迟初始化:如果想避免在程序启动时就实例化,可以使用 std::call_once
  • 生命周期管理:若需要在程序结束前释放资源,考虑 std::unique_ptrstd::shared_ptr
  • 性能考虑:在多线程读多写少的场景下,读写锁可进一步提升性能,但要权衡实现复杂度。

通过以上方法,你可以根据项目需求灵活选择最合适的单例实现方案,确保代码既简洁又安全。

发表评论