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

单例模式(Singleton)是一种常见的软件设计模式,用来保证某个类只有一个实例,并提供一个全局访问点。虽然实现单例在单线程环境中相对简单,但在多线程环境下,需要注意线程安全性,防止出现双重检查锁定(double-checked locking)导致的竞态条件。下面介绍几种在C++中实现线程安全单例的常用方法,并讨论各自的优缺点。

1. 采用局部静态变量(C++11之后)

C++11 引入了对局部静态变量的线程安全初始化保证。使用这种方式,最简洁且性能最优:

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

优点

  • 代码简洁,几乎没有运行时开销。
  • 通过语言层面保证线程安全,无需显式锁。

缺点

  • 只在 C++11 及以后可用。
  • 如果 Singleton 的构造函数抛异常,随后再次访问 instance() 时会再次尝试构造。

2. 使用 std::call_oncestd::once_flag

std::call_oncestd::once_flag 是 C++11 提供的线程同步原语,专门用于一次性初始化:

#include <mutex>

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

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

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

std::once_flag Singleton::initFlag_;
Singleton* Singleton::instancePtr_ = nullptr;

优点

  • 明确表达“一次性初始化”的语义。
  • 兼容旧版本编译器(只需 C++11)。

缺点

  • 需要手动管理单例指针,易产生内存泄漏(虽然这里使用裸指针,但一般可用 std::unique_ptr)。
  • 仍然有一点额外的同步开销(一次 std::call_once)。

3. 双重检查锁定(DCL)+ std::atomic

传统的双重检查锁定需要使用 std::atomicstd::volatile 以保证内存可见性。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;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

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

优点

  • 只在第一次创建时使用锁,之后访问几乎无锁。

缺点

  • 代码复杂,易出错。
  • 需要仔细使用内存顺序,错误的顺序会导致未定义行为。
  • 在 C++11 前的编译器不支持 std::atomic,难以实现。

4. 静态局部变量 + 显式销毁(Meyers 单例)

如果你希望在程序结束时显式销毁单例,可以结合 std::unique_ptr

class Singleton {
public:
    static Singleton& instance() {
        static std::unique_ptr <Singleton> ptr(new Singleton());
        return *ptr;
    }
    // ...
private:
    Singleton() = default;
};

优点

  • 自动销毁,避免内存泄漏。
  • 线程安全(C++11)。

缺点

  • 仍然依赖 C++11 的线程安全初始化。

5. 何时使用哪种实现?

场景 推荐实现
简单项目,C++11+ 局部静态变量(Meyers)
需要手动管理生命周期或兼容旧编译器 std::call_once
性能极端敏感,且需要自定义内存管理 DCL + std::atomic
需要在多线程下进行自定义一次性初始化 std::call_once

6. 单例的陷阱

  1. 全局资源竞争:单例往往被过度使用,导致过多的全局共享状态,容易出现线程不安全。
  2. 测试难度:单例难以替换,单元测试时需使用全局状态来模拟,代码耦合度高。
  3. 资源释放顺序:如果单例持有其他全局资源,释放顺序需要仔细设计,避免悬空指针。

7. 结语

在 C++ 中实现线程安全单例,最推荐的做法是利用 C++11 的局部静态变量特性。它既简单又高效,同时无需手动同步,代码也更易维护。只有在特殊需求下才考虑使用 std::call_once 或双重检查锁定。无论选择哪种实现方式,都请确保对单例的使用场景有清晰的认识,避免滥用导致的并发难题。

发表评论