在 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() {}
};
- 优点:实现极简,编译器负责线程同步。
- 缺点:若需要在对象构造前检查错误,可能不够灵活。
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []() { instance.reset(new Singleton); });
return *instance;
}
private:
Singleton() {}
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
- 优点:可以在构造前做错误处理,初始化逻辑更可控。
- 缺点:代码稍显繁琐。
3. 双重检查锁(DCLP)— 传统实现
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex);
if (!instance) { // 第二次检查
instance = new Singleton;
}
}
return instance;
}
private:
Singleton() {}
static Singleton* instance;
static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
- 优点:对老旧编译器兼容。
- 缺点:若不小心实现不对,容易出现指令重排导致线程不安全。C++11 的内存模型已经足够安全,通常不建议手写 DCLP。
4. std::shared_ptr 与 std::weak_ptr(懒加载与销毁)
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(initFlag, []() {
instance = std::shared_ptr <Singleton>(new Singleton);
});
return instance;
}
private:
Singleton() {}
static std::shared_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::shared_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
- 优点:可以自动管理生命周期,支持多处引用。
- 缺点:需要注意循环引用导致内存泄漏。
5. 采用 std::atomic 与 std::mutex 的组合(更通用的实现)
class Singleton {
public:
static Singleton& getInstance() {
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() {}
static std::atomic<Singleton*> instance;
static std::mutex mutex;
};
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mutex;
- 优点:显式控制内存顺序,满足极高性能要求。
- 缺点:实现相对复杂,易出错。
何时使用哪种实现?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 需要最简洁实现,且不需要在构造时检查错误 | Meyers 单例 | 只需一行代码即可 |
| 构造过程中可能抛异常或需要做额外检查 | std::call_once |
支持错误处理 |
| 需要支持多处销毁(计数式) | std::shared_ptr |
自动管理生命周期 |
| 对老旧编译器(C++11 之前)兼容 | 双重检查锁 | 需谨慎实现 |
| 性能极限(微秒级) | std::atomic + std::mutex |
细粒度内存顺序控制 |
常见错误与调试技巧
-
双重检查锁未加
std::atomic
在 C++11 之后,std::atomic或std::memory_order可保证可见性。若省略,会出现“空指针解引用”的隐蔽错误。 -
忘记
delete
如果使用裸指针,需要在适当时机删除,防止内存泄漏。std::unique_ptr或std::shared_ptr可以自动完成。 -
构造函数抛异常
std::call_once在异常后会重新尝试初始化,但需注意std::once_flag在异常后仍可重用。 -
多线程读写顺序
通过std::memory_order_acquire/release可以细粒度控制访问顺序,避免缓存不一致。
小结
C++11 之后,线程安全单例实现几乎不需要手写锁,而是利用编译器和标准库提供的同步原语。选择合适的实现方式,既能保持代码简洁,又能满足特定需求。掌握这些模式后,写出既安全又高效的单例组件将不再是难题。