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

单例模式(Singleton Pattern)是设计模式之一,其核心思想是确保一个类在整个程序生命周期内只有一个实例,并且为全局提供访问点。在C++中实现线程安全的单例模式,一般有以下几种常见方式:

  1. Meyers 单例(C++11 之后的局部静态变量)
  2. 双重检查锁(Double‑Check Locking)
  3. std::call_once + std::once_flag

下面分别介绍并给出示例代码。


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

C++11 起,局部静态变量的初始化是线程安全的。最简洁、最推荐的方式:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 之后线程安全
        return instance;
    }

    // 其他公共接口
    void doSomething() { /* ... */ }

private:
    Singleton() { /* 构造逻辑 */ }
    ~Singleton() { /* 析构逻辑 */ }

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 简单易懂
  • 编译器保证线程安全
  • 延迟初始化(第一次调用 instance() 时才创建)

缺点

  • 在某些老旧编译器(C++11 之前)不可行
  • 如果构造函数抛异常,后续调用仍会继续尝试重新初始化(但同样是线程安全的)

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

适用于旧编译器或需要自定义初始化逻辑时。关键是使用 std::atomicvolatile 与互斥量结合。

#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() {}
    ~Singleton() {}

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

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

注意事项

  • instance_ 必须是 std::atomic,否则并发读写会出现数据竞争。
  • std::memory_order 的使用确保正确的可见性。
  • 仍然要防止析构时多线程访问的问题。

3. std::call_once + std::once_flag

这是 C++11 标准库提供的最安全、最简洁的实现方式:

#include <mutex>

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

    // ...

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

    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

优点

  • call_once 确保初始化只执行一次,即使多线程并发访问。
  • 不需要手动使用互斥锁。

缺点

  • 仍然使用裸指针,需要自行管理析构。可以改为 std::unique_ptr

4. 何时使用哪种方式?

方案 适用场景 主要特点
Meyers 单例 C++11 及以上 简洁,编译器保证线程安全
双重检查锁 旧编译器或需要自定义构造 需要手动锁,复杂度较高
call_once C++11 及以上,需对初始化做额外操作 线程安全,易于使用

在实际项目中,首选 Meyers 单例,除非你需要在单例构造时做一些复杂的同步操作(例如读取配置文件、建立数据库连接),此时 std::call_once 会更合适。


5. 小结

实现线程安全的单例在 C++ 中非常成熟。利用标准库提供的特性,既可以保证代码的可维护性,又能避免手动管理锁导致的错误。推荐在项目中使用 Meyers 单例std::call_once,这两种方式足以满足绝大多数需求,并且代码简洁、易读。

发表评论