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

在多线程环境下,保证单例对象只被实例化一次是一项挑战。下面我们从几种常见实现方式出发,演示如何在 C++ 中实现线程安全的单例模式,并讨论各自的优缺点。


1. 经典 Meyers 单例(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;
};
  • 优点

    • 代码简洁,易于维护。
    • 依赖编译器的 static 局部变量初始化的线程安全保证(C++11 之后)。
    • 延迟初始化:第一次调用 instance() 时才构造对象。
  • 缺点

    • 如果单例需要在程序结束前做特殊清理,无法显式控制销毁顺序。
    • 对于需要参数化构造的单例,无法直接使用。

2. std::call_oncestd::once_flag

#include <mutex>

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

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

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

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

std::unique_ptr <Singleton> Singleton::ptr = nullptr;
std::once_flag Singleton::initFlag;
  • 优点

    • 可以在单例构造函数中传递参数(通过 lambda 传递)。
    • 对构造过程的控制更细,适合需要按需初始化的场景。
    • 线程安全性明确显式,易于阅读。
  • 缺点

    • 代码略显繁琐。
    • std::unique_ptr 需要额外的头文件支持。

3. 双重检查锁(Double-Check Locking)——注意 C++ 的实现细节

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = ptr;
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = ptr;
            if (!tmp) {
                ptr = tmp = new Singleton();
            }
        }
        return tmp;
    }

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

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

    static Singleton* ptr;
    static std::mutex mtx;
};

Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
  • 优点

    • 第一次调用时无锁,后续调用无需加锁,性能更好。
  • 缺点

    • 需要在构造函数中保证对 ptr 的写操作是可见的(使用 std::atomicstd::memory_order)。
    • 代码难以维护,易出错。
    • 对于 C++11 之前的编译器,可能不安全。

4. 静态成员指针 + std::atomic(现代实现)

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

std::atomic<Singleton*> Singleton::ptr{nullptr};
std::mutex Singleton::mtx;
  • 优点

    • 明确使用原子操作保证可见性,符合 C++11 的内存模型。
    • 与双重检查锁类似,性能优越。
  • 缺点

    • 仍然需要 std::mutex 作为同步手段,代码稍显繁琐。

5. 何时使用哪种实现?

场景 推荐实现
单例无参数、只读 Meyers 单例
需要按需参数化构造 std::call_once
对性能要求极高,且对 C++11/17 兼容 双重检查 + std::atomic
需要手动控制单例生命周期 静态成员 + std::call_once(结合智能指针)

6. 小结

  • 线程安全:C++11 之后,局部静态变量的初始化已经线程安全,推荐使用 Meyers 单例。
  • 可配置性:若需要在单例构造时传参,std::call_once 能提供足够的灵活性。
  • 性能:双重检查锁和原子指针结合可以在极端高并发场景中减少锁开销。

在实际项目中,往往选择最简单、最易维护的实现方式(Meyers 单例)即可满足大多数需求。只有在特殊情况下才需要更复杂的同步方案。

发表评论