在多线程环境下,传统的单例实现往往会出现竞争条件,导致多次实例化或数据破坏。C++11 引入的线程库和原子操作提供了多种安全的实现方式。下面先从经典的「Meyers 单例」谈起,再讨论 std::call_once、std::atomic 以及 std::mutex 的组合方案,最后给出一个可扩展的、线程安全且延迟初始化的单例模板。
1. Meyers 单例(C++11 线程安全的实现)
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
static ThreadSafeSingleton instance; // C++11 保证线程安全
return instance;
}
// 删除拷贝构造和赋值
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
void doSomething() { /* ... */ }
private:
ThreadSafeSingleton() { /* 可能耗时的初始化 */ }
~ThreadSafeSingleton() = default;
};
原理:C++11 规定 static 局部变量在第一次进入作用域时会被初始化,且此过程是原子化的。若多线程并发访问,编译器会在内部插入一个同步锁,确保只会有一次初始化。
优点:
- 简洁,几行代码即可实现。
- 自动销毁(栈式析构)。
缺点:
- 延迟初始化(首次调用
getInstance时才创建)。如果程序的启动阶段需要提前创建实例,可手动触发一次getInstance()。
2. std::call_once + std::once_flag
std::call_once 允许你在多线程环境中仅执行一次指定函数。适用于需要在构造函数外完成复杂初始化的场景。
class InitOnDemandSingleton {
public:
static InitOnDemandSingleton& getInstance() {
std::call_once(initFlag, []() {
instance.reset(new InitOnDemandSingleton);
});
return *instance;
}
// 其他成员...
private:
InitOnDemandSingleton() { /* 复杂初始化 */ }
static std::unique_ptr <InitOnDemandSingleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <InitOnDemandSingleton> InitOnDemandSingleton::instance;
std::once_flag InitOnDemandSingleton::initFlag;
优点:
- 初始化代码完全由你控制(如读取配置文件、网络请求等)。
- 适用于在初始化时可能抛异常,需要捕获并重试的情况。
缺点:
- 代码略显冗长,需维护
once_flag与unique_ptr。
3. 采用原子指针 + 双检锁
如果你想在 C++11 之前的环境下实现线程安全(如 C++98/03),可以使用双检锁(Double-Check Locking)并配合 std::atomic 或者平台原子操作。
#include <atomic>
#include <mutex>
class AtomicSingleton {
public:
static AtomicSingleton* getInstance() {
AtomicSingleton* 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 AtomicSingleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
AtomicSingleton() { /* ... */ }
static std::atomic<AtomicSingleton*> instance;
static std::mutex mtx;
};
std::atomic<AtomicSingleton*> AtomicSingleton::instance{nullptr};
std::mutex AtomicSingleton::mtx;
注意:双检锁在某些平台(如 ARM 的弱一致性)下仍存在潜在的问题。C++11 的 static 方式已解决大多数情形。
4. 可扩展的线程安全单例模板
为了在多个类中复用单例实现,下面给出一个简洁的模板。它使用 std::call_once 并且提供了 create() 供子类自定义实例化过程。
#include <memory>
#include <mutex>
template <typename T>
class Singleton {
public:
static T& getInstance() {
std::call_once(initFlag, []() {
instance.reset(new T);
});
return *instance;
}
// 防止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
protected:
Singleton() = default;
virtual ~Singleton() = default;
private:
static std::unique_ptr <T> instance;
static std::once_flag initFlag;
};
template <typename T>
std::unique_ptr <T> Singleton<T>::instance{nullptr};
template <typename T>
std::once_flag Singleton <T>::initFlag;
// 使用示例
class Logger : public Singleton <Logger> {
friend class Singleton <Logger>;
private:
Logger() { /* 打开日志文件 */ }
public:
void log(const std::string& msg) { /* 写日志 */ }
};
// 访问方式
// Logger::getInstance().log("Hello");
优点:
- 只需一次声明,所有继承类都可获得线程安全单例。
- 代码可读性高,维护简单。
5. 小结
- Meyers 单例:最简洁、最安全,适合大多数场景。
- std::call_once:适用于需要复杂初始化或异常处理的情况。
- 双检锁 + 原子:兼容旧标准,但需谨慎使用。
- 模板化单例:复用性强,适合大型项目。
在实际项目中,建议先从 Meyers 单例 开始,若需要自定义初始化过程再考虑 std::call_once 或模板方案。这样既能保持代码简洁,又能满足多线程安全的要求。