在多线程环境下,单例模式需要特别小心,以避免多线程竞争导致多个实例被创建。下面介绍几种常见且线程安全的实现方式,并给出示例代码。
1. C++11 的 std::call_once 与 std::once_flag
C++11 标准提供了 std::call_once 与 std::once_flag,可以确保某段代码只被执行一次,并且对多线程是安全的。
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() {
instancePtr.reset(new Singleton);
});
return *instancePtr;
}
// 删除拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
解释
std::once_flag用于标记一次性初始化。std::call_once接收once_flag和一个可调用对象(lambda),保证该可调用对象只会被执行一次。instancePtr是一个unique_ptr,负责管理单例对象的生命周期,确保在程序结束时正确销毁。
2. 局部静态变量(Meyers单例)
在 C++11 之后,局部静态变量的初始化是线程安全的。实现非常简洁。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
static Singleton instance;在第一次调用instance()时初始化,后续调用直接返回。- C++11 标准保证了此初始化过程是互斥的,避免了竞争条件。
3. 双重检查锁(Double-Check Locking)
虽然在 C++11 之后不再需要,因为前两种方式已经足够简单可靠,但为了完整性,以下给出经典的双重检查锁实现。需要使用 std::atomic 或 std::mutex 以及 std::memory_order。
#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;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
- 首先使用
std::memory_order_acquire读取instance_,如果为nullptr再加锁。 - 加锁后再次检查,避免多线程重复创建。
- 这种实现需要精确掌握内存序和锁粒度,容易出错,建议使用前两种更安全、简洁的方法。
4. 延迟初始化与智能指针
如果单例需要在程序退出时安全析构,可将实例包装在 std::unique_ptr 并在 std::atexit 注册销毁函数。
#include <memory>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){
instancePtr.reset(new Singleton);
std::atexit(&Singleton::destroy);
});
return *instancePtr;
}
// ...
private:
static void destroy() {
instancePtr.reset();
}
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::atexit确保在main结束后调用destroy,安全释放资源。- 适用于需要在程序退出前做清理工作的单例。
5. 适用场景与选择建议
| 方法 | 特点 | 推荐场景 |
|---|---|---|
std::call_once |
线程安全,显式控制 | 需要按需初始化,或想在不同线程中动态决定是否创建 |
| 局部静态变量 | 简洁、易懂 | 绝大多数情况,推荐首选 |
| 双重检查锁 | 传统实现 | 仅在极端性能需求时考虑,建议避免 |
| 延迟销毁 | 需要控制析构顺序 | 复杂资源依赖或需要在 atexit 时清理 |
6. 小结
- 在 C++11 之后,最推荐的实现是局部静态变量,既简洁又安全。
- 若需要更细粒度的控制,
std::call_once与std::once_flag是最佳选择。 - 双重检查锁虽然经典,但易出现错误,除非你了解所有细节,否则不要使用。
- 结合
std::unique_ptr与std::atexit可以安全地管理单例的生命周期。
通过上述实现,你可以在多线程 C++ 应用中安全、可靠地使用单例模式。