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

在 C++ 中实现单例模式时,线程安全是一个关键考虑因素。下面介绍几种常见且安全的实现方式,并比较它们的优缺点。

1. Meyers 单例(局部静态变量)

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;
};
  • 优点:代码简洁,编译器保证初始化是线程安全的(自 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_;
  • 优点:延迟初始化(第一次访问时才创建),适合需要控制实例创建时机的场景。
  • 缺点:代码较为复杂,容易出现细节错误。需要保证内存顺序和正确的同步。

3. 静态局部对象 + 互斥锁(手动控制)

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){
            instance_ = new Singleton();
        });
        return *instance_;
    }

    // 必须保证删除器线程安全
    static void destroy() {
        std::call_once(destroy_flag_, [](){
            delete instance_;
            instance_ = nullptr;
        });
    }

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

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

    static Singleton* instance_;
    static std::once_flag flag_;
    static std::once_flag destroy_flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
std::once_flag Singleton::destroy_flag_;
  • 优点:使用 std::call_once 保证一次性初始化,且语义清晰。可以在需要时手动销毁实例。
  • 缺点:需要手动调用 destroy(),否则会在程序退出时由系统析构。

4. 基于 std::shared_ptr 的单例(懒加载)

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(init_flag_, [](){
            ptr_ = std::shared_ptr <Singleton>(new Singleton);
        });
        return ptr_;
    }

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

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

    static std::shared_ptr <Singleton> ptr_;
    static std::once_flag init_flag_;
};

std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::init_flag_;
  • 优点:返回 std::shared_ptr,方便与其他代码共享生命周期。自动析构。
  • 缺点:共享计数导致额外开销。若单例本身不需要被复制,使用裸指针更合适。

5. 线程安全与性能权衡

  • 一次性初始化:如果实例不需要懒加载,直接使用 Meyers 单例即可,既安全又高效。
  • 延迟创建:若实例初始化代价高且不确定是否会被使用,建议使用双重检查锁或 std::call_once
  • 跨线程访问:所有方法均保证多线程安全,关键点是使用 std::atomicstd::mutexstd::call_once
  • 销毁顺序:在多模块依赖单例时,注意销毁顺序。使用 std::call_oncedestroystd::shared_ptr 可以帮助管理生命周期。

6. 小结

  • 推荐:对大多数项目,使用 C++11 之后的局部静态变量(Meyers 单例)是最简单、最安全、性能最优的方案。
  • 特殊需求:若需要延迟加载、显式销毁或自定义内存管理,可考虑 std::call_once 或双重检查锁实现。

通过以上方法,你可以在 C++ 项目中灵活、安全地实现单例模式,满足不同场景下的需求。

发表评论