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

在多线程环境下,单例模式需要保证同一时刻只有一个实例被创建,并且所有线程都能安全地访问该实例。下面介绍几种常见的实现方式,并比较它们的优缺点。


1. 使用 std::call_oncestd::once_flag

C++11 标准库提供了 std::call_oncestd::once_flag,可以在多线程中安全地初始化单例。

#include <mutex>
#include <memory>

class Singleton {
public:
    static Singleton& instance() {
        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_;

优点

  • 线程安全std::call_once 保证初始化只执行一次。
  • 代码简洁:不需要手动锁或原子操作。
  • 延迟初始化:仅在第一次调用 instance() 时创建。

缺点

  • C++11 依赖:只能在支持 C++11 及以后标准的编译器中使用。
  • 静态对象:在销毁时可能出现“静态析构顺序问题”,但因为使用 unique_ptr,在程序结束前会被销毁。

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

经典的双重检查锁实现仍然在一些代码库中出现,虽然在 C++11 前不安全,但在 C++11 之后由于内存模型的改进,可以安全使用。

#include <atomic>
#include <mutex>

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_;

优点

  • 延迟初始化:与 call_once 相同。
  • 可读性:在某些团队中更易于理解。

缺点

  • 复杂度高:需要手动管理原子操作与锁。
  • 潜在错误:如果不严格遵守 std::memory_order,可能导致数据竞争。

3. 静态局部变量(Meyers’ Singleton)

最简洁、最安全的实现方式是利用 C++11 之后的局部静态变量初始化线程安全的特性。

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

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

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

优点

  • 代码最短:几行代码即可完成。
  • 自动销毁:在程序结束时自动销毁。
  • 线程安全:C++11 起保证局部静态变量初始化是线程安全的。

缺点

  • 延迟销毁:如果想手动销毁单例,需要额外设计。
  • 不可跨编译单元:若在多个源文件中调用,可能导致不同实例,除非把 instance() 放在头文件并使用 inlineconstexpr

4. 总结与最佳实践

实现方式 线程安全 延迟初始化 代码量 兼容性
std::call_once 中等 C++11+
双重检查锁 C++11+
Meyers’ Singleton C++11+
显式 static 成员 C++11+

建议

  1. 优先使用 Meyers’ Singleton:最简洁且安全,适合大多数场景。
  2. 若需要手动销毁或延迟释放,使用 std::call_onceunique_ptr 结合。
  3. 避免使用全局 new/delete:会产生内存泄漏或析构顺序问题。
  4. 保持单例的不可复制性:删除拷贝构造函数与赋值运算符。

通过以上几种实现,你可以根据项目需求、编译器支持与代码规范选择最合适的单例模式。祝编码愉快!

发表评论