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

在多线程环境下,单例模式需要特别小心,以避免多线程竞争导致多个实例被创建。下面介绍几种常见且线程安全的实现方式,并给出示例代码。

1. C++11 的 std::call_oncestd::once_flag

C++11 标准提供了 std::call_oncestd::once_flag,可以确保某段代码只被执行一次,并且对多线程是安全的。

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, []() {
            instancePtr.reset(new Singleton);
        });
        return *instancePtr;
    }

    // 删除拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

    static std::unique_ptr <Singleton> instancePtr;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

解释

  • std::once_flag 用于标记一次性初始化。
  • std::call_once 接收 once_flag 和一个可调用对象(lambda),保证该可调用对象只会被执行一次。
  • instancePtr 是一个 unique_ptr,负责管理单例对象的生命周期,确保在程序结束时正确销毁。

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

在 C++11 之后,局部静态变量的初始化是线程安全的。实现非常简洁。

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;
    ~Singleton() = default;
};
  • static Singleton instance; 在第一次调用 instance() 时初始化,后续调用直接返回。
  • C++11 标准保证了此初始化过程是互斥的,避免了竞争条件。

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

虽然在 C++11 之后不再需要,因为前两种方式已经足够简单可靠,但为了完整性,以下给出经典的双重检查锁实现。需要使用 std::atomicstd::mutex 以及 std::memory_order

#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_;
  • 首先使用 std::memory_order_acquire 读取 instance_,如果为 nullptr 再加锁。
  • 加锁后再次检查,避免多线程重复创建。
  • 这种实现需要精确掌握内存序和锁粒度,容易出错,建议使用前两种更安全、简洁的方法。

4. 延迟初始化与智能指针

如果单例需要在程序退出时安全析构,可将实例包装在 std::unique_ptr 并在 std::atexit 注册销毁函数。

#include <memory>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, [](){
            instancePtr.reset(new Singleton);
            std::atexit(&Singleton::destroy);
        });
        return *instancePtr;
    }

    // ...

private:
    static void destroy() {
        instancePtr.reset();
    }

    static std::unique_ptr <Singleton> instancePtr;
    static std::once_flag initFlag;
};
  • std::atexit 确保在 main 结束后调用 destroy,安全释放资源。
  • 适用于需要在程序退出前做清理工作的单例。

5. 适用场景与选择建议

方法 特点 推荐场景
std::call_once 线程安全,显式控制 需要按需初始化,或想在不同线程中动态决定是否创建
局部静态变量 简洁、易懂 绝大多数情况,推荐首选
双重检查锁 传统实现 仅在极端性能需求时考虑,建议避免
延迟销毁 需要控制析构顺序 复杂资源依赖或需要在 atexit 时清理

6. 小结

  • 在 C++11 之后,最推荐的实现是局部静态变量,既简洁又安全。
  • 若需要更细粒度的控制,std::call_oncestd::once_flag 是最佳选择。
  • 双重检查锁虽然经典,但易出现错误,除非你了解所有细节,否则不要使用。
  • 结合 std::unique_ptrstd::atexit 可以安全地管理单例的生命周期。

通过上述实现,你可以在多线程 C++ 应用中安全、可靠地使用单例模式。

发表评论