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

在现代 C++(C++11 及以后)中,线程安全的单例模式可以通过几种方式实现,其中最简洁、最可靠的方法是利用局部静态变量的“魔法”以及标准库的原语。下面详细说明几种实现方式,并对其优缺点进行比较。

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

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11保证线程安全的初始化
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() { /* 资源初始化 */ }
    ~Singleton() { /* 资源释放 */ }
};

优点

  • 代码简洁,只需一行。
  • 编译器自动保证初始化时的线程安全(C++11 标准)。
  • 延迟初始化(懒加载),只有第一次调用 getInstance() 时才构造对象。

缺点

  • 不能控制实例析构的时机(仅在程序结束时析构)。
  • 对于某些极端多线程场景(极高竞争)可能产生轻微的锁争用。

2. 双重检查锁(Double-Checked Locking) + std::atomic

#include <atomic>
#include <mutex>

class Singleton {
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    // 其他成员...

private:
    Singleton() {}
    ~Singleton() {}

    static std::atomic<Singleton*> instance;
    static std::mutex mtx;
};

std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;

优点

  • 只在第一次创建实例时加锁,后续访问无锁,性能高。
  • 可在需要时手动销毁实例(通过 delete instancestd::unique_ptr 包装)。

缺点

  • 代码较为复杂,容易出错。
  • 对 C++11 内存模型的正确使用要求较高。

3. std::call_oncestd::once_flag

#include <mutex>

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(flag, []() { instance.reset(new Singleton()); });
        return *instance;
    }
private:
    Singleton() {}
    ~Singleton() {}
    static std::unique_ptr <Singleton> instance;
    static std::once_flag flag;
};

std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::flag;

优点

  • 代码比双重检查锁更简洁。
  • std::call_once 内部实现已保证线程安全且无竞争开销。

缺点

  • std::atomic 相比,仍存在一次锁操作,但几乎可以忽略。

4. 结合 std::shared_ptrstd::weak_ptr

如果单例需要在多处共享,并可能在程序运行期间被销毁后重新创建,可以考虑:

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (auto ptr = instance.lock()) {
            return ptr;
        }
        auto newInstance = std::make_shared <Singleton>();
        instance = newInstance;
        return newInstance;
    }
private:
    Singleton() {}
    static std::weak_ptr <Singleton> instance;
    static std::mutex mtx;
};

std::weak_ptr <Singleton> Singleton::instance;
std::mutex Singleton::mtx;

优点

  • 可在多线程间安全共享实例。
  • 支持在实例被销毁后自动重建。

缺点

  • 引入了引用计数,略微影响性能。
  • 需要手动管理锁。

5. 对比与最佳实践

实现方式 线程安全保障 性能 可定制性 代码复杂度
局部静态 自动(C++11) 高(无锁) 低(仅在程序退出析构)
双重检查锁 手动(原子 + mutex) 很高 高(可手动销毁)
call_once 自动(once_flag)
weak_ptr + mutex 手动 高(可复用)
  • 如果你只需要一个简单的、全局生命周期的单例,推荐使用 局部静态变量(Meyers 单例)。它是最安全、最简单且符合现代 C++ 标准的实现。
  • 如果需要手动销毁或在多次调用间重建实例,建议使用 std::call_once + std::unique_ptr双重检查锁(后者更适合极端性能要求场景)。
  • 如果单例需要在不同线程间共享且可能被销毁后重建,使用 std::shared_ptr + std::weak_ptr 方案。

6. 常见陷阱

  1. C++03 版本:局部静态变量的初始化不是线程安全的,必须手动加锁或使用 std::mutex
  2. 多线程递归:如果单例在构造过程中再次调用 getInstance(),要确保使用 std::call_once 或局部静态,否则可能导致死锁或未定义行为。
  3. 静态变量初始化顺序:跨文件的静态单例在不同翻译单元中可能产生“静态初始化顺序问题”。使用局部静态可以规避此问题。
  4. 内存泄漏:使用 new 的单例需要手动 delete 或使用智能指针避免泄漏。

7. 结语

C++11 以后,单例模式的实现已大大简化。最安全、最简洁的方案是局部静态变量,但在需要更细粒度控制时,std::call_once 或双重检查锁仍然是不错的选择。掌握好线程安全和资源管理的细节,能让你在并发程序中放心使用单例而不必担心竞争条件。

发表评论