在现代C++中,单例模式经常被用于提供全局访问点,尤其是在需要跨线程共享资源时。实现一个线程安全、延迟初始化且开销极小的单例,一直是程序员关注的热点。本文从需求出发,梳理几种常见实现,重点探讨C++11及其后版本的线程安全特性,最后给出完整可直接使用的代码示例。
1. 需求背景
- 全局共享:某些资源(如日志系统、配置管理器)在整个应用生命周期内只需要创建一次。
- 延迟初始化:避免在程序启动时就完成昂贵的初始化,只有真正需要时才构造对象。
- 线程安全:多线程环境下,必须保证单例对象只被构造一次且不会出现竞态条件。
2. 传统实现(非线程安全)
class LegacySingleton {
public:
static LegacySingleton& instance() {
static LegacySingleton* p = new LegacySingleton();
return *p;
}
private:
LegacySingleton() = default;
LegacySingleton(const LegacySingleton&) = delete;
LegacySingleton& operator=(const LegacySingleton&) = delete;
};
上述实现只在第一次调用instance()时创建对象,随后直接返回已存在的对象。然而,在多线程同时访问时,两个线程可能同时进入instance()函数,导致产生两个实例,破坏单例性质。
3. Meyers Singleton(C++11之后线程安全)
C++11引入了对静态局部变量初始化的线程安全保证。
class MeyersSingleton {
public:
static MeyersSingleton& instance() {
static MeyersSingleton inst; // 线程安全初始化
return inst;
}
private:
MeyersSingleton() = default;
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator=(const MeyersSingleton&) = delete;
};
- 优点:代码简洁,延迟初始化,线程安全。
- 缺点:若构造函数抛异常,后续调用将再次尝试构造,直到成功。
- 适用场景:大多数情况已足够。
4. std::call_once + std::once_flag
如果你需要在构造对象之前执行一次性初始化操作,或者在C++11之前的环境中(比如某些嵌入式编译器),可以使用std::call_once。
class CallOnceSingleton {
public:
static CallOnceSingleton& instance() {
std::call_once(initFlag, [](){ pInstance = new CallOnceSingleton(); });
return *pInstance;
}
private:
CallOnceSingleton() = default;
~CallOnceSingleton() { delete pInstance; }
static CallOnceSingleton* pInstance;
static std::once_flag initFlag;
CallOnceSingleton(const CallOnceSingleton&) = delete;
CallOnceSingleton& operator=(const CallOnceSingleton&) = delete;
};
CallOnceSingleton* CallOnceSingleton::pInstance = nullptr;
std::once_flag CallOnceSingleton::initFlag;
- 优点:可控制初始化过程,例如先加载配置再实例化。
- 缺点:手动管理内存,需要在程序结束时显式删除或使用
std::unique_ptr。
5. C++17 inline 变量(更简洁)
C++17引入了inline变量,允许在头文件中定义静态成员,减少一次性初始化的复杂度。
class InlineSingleton {
public:
static InlineSingleton& instance() {
return inst;
}
private:
InlineSingleton() = default;
static inline InlineSingleton inst; // inline 变量
InlineSingleton(const InlineSingleton&) = delete;
InlineSingleton& operator=(const InlineSingleton&) = delete;
};
这与Meyers Singleton在功能上等价,但显式声明了静态成员,方便阅读。
6. 完整代码示例(线程安全+异常安全+RAII)
#include <iostream>
#include <mutex>
class SafeSingleton {
public:
static SafeSingleton& getInstance() {
// 通过call_once保证只调用一次构造
std::call_once(initFlag, [](){
instance.reset(new SafeSingleton);
});
return *instance;
}
// 示例接口
void doSomething() {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "Singleton instance address: " << this << std::endl;
}
private:
SafeSingleton() = default;
~SafeSingleton() = default;
// 禁止拷贝和赋值
SafeSingleton(const SafeSingleton&) = delete;
SafeSingleton& operator=(const SafeSingleton&) = delete;
static std::once_flag initFlag;
static std::unique_ptr <SafeSingleton> instance;
std::mutex mutex_;
};
std::once_flag SafeSingleton::initFlag;
std::unique_ptr <SafeSingleton> SafeSingleton::instance = nullptr;
使用示例
#include <thread>
void worker() {
SafeSingleton::getInstance().doSomething();
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
return 0;
}
运行结果显示两次调用输出相同的地址,证明只创建了一个实例。
7. 性能对比
- Meyers Singleton:最简洁,几乎无额外开销,构造时有一次原子检查。
- std::call_once:多线程场景下多了一次锁/信号量的开销,但在单例已创建后几乎无额外成本。
- inline 变量:与Meyers等价,但更易维护。
根据实际需求,若仅需单例,Meyers实现已足够;若需要在单例构造前做一次性初始化或在C++11前环境,可选call_once。
8. 结论
- 推荐:在C++11及以后,优先使用Meyers Singleton或inline变量实现;只在特殊初始化需求时使用
std::call_once。 - 注意:若单例在销毁时需要执行清理,务必使用RAII或显式删除,避免内存泄漏。
- 测试:在多线程环境下,建议用工具(如ThreadSanitizer)验证单例实现的线程安全性。
通过本文的对比与示例,读者可以快速选择合适的单例实现,并在项目中稳健使用。