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

在 C++ 里,单例模式是用来保证一个类只有一个实例并且提供全局访问点的一种设计模式。若不加以注意,单例的实现往往会出现线程安全问题,尤其是在多线程环境下。下面给出几种在 C++11 及以后版本中实现线程安全单例的常见方案,并分析它们的优缺点。

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;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 优点
    • 代码简洁,易于理解。
    • 由编译器保证初始化的线程安全。
    • 延迟初始化,只有第一次调用 instance() 时才会创建对象。
  • 缺点
    • 不能在实例化时传参。
    • 对象销毁顺序与其他全局静态对象相互影响,可能导致析构时访问已被销毁的资源。

2. std::call_oncestd::once_flag

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

    // 允许显式销毁(如需要)
    static void destroy() {
        delete instancePtr_;
        instancePtr_ = nullptr;
    }

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

    static Singleton* instancePtr_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;
  • 优点
    • 线程安全且显式控制实例化时机。
    • 可以传参给构造函数,只需在 lambda 中完成。
  • 缺点
    • 需要手动管理单例指针(虽然是单例)。
    • 需要额外的头文件 ` `。

3. C++17 的 inline static 成员

class Singleton {
public:
    static Singleton& instance() {
        return instance_;
    }

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

    inline static Singleton instance_;  // C++17 允许 inline static 成员
};
  • 优点
    • 代码更简洁,省去了 call_once
    • 仍然保证线程安全的初始化。
  • 缺点
    • 只能在 C++17 及以上使用。
    • Meyers 相同,无法传参。

4. 原子指针与双重检查锁定(Double-Check 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;
    }

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

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

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
  • 优点
    • 对于只读使用,后续访问几乎无锁。
  • 缺点
    • 代码复杂,容易出现错误。
    • 需要保证构造函数是线程安全的。
    • 需要手动销毁单例,或者使用 std::unique_ptr 自动管理。

5. 智能指针封装

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        static std::shared_ptr <Singleton> ptr(new Singleton);
        return ptr;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 优点
    • 自动析构,资源释放更安全。
  • 缺点
    • 需要 C++11 及以上。
    • 共享计数会有轻微性能开销。

小结

  • 最简单、最推荐:局部静态变量(Meyers Singleton),只要不需要在构造时传参即可。
  • 需要传参或自定义初始化顺序:使用 std::call_once + std::once_flag
  • C++17 以上:可使用 inline static 成员。
  • 高性能需求:若对首次实例化后的访问速度极度敏感,可考虑原子指针 + 双重检查锁定,但实现成本高且易错。

在实际项目中,建议先评估是否真的需要单例模式;若仅是想要全局共享对象,使用 std::shared_ptr 或全局对象并配合 std::once_flag 进行延迟初始化,也是一个不错的方案。

发表评论