在多线程环境下,单例(Singleton)模式需要保证实例的唯一性和线程安全。自C++11起,语言层面已经提供了对线程安全初始化的支持,简化了实现。下面从理论到代码演示几种常用的实现方式,并讨论它们的优缺点。
1. C++11 的“懒汉式”Meyers Singleton
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 函数内静态对象
return instance;
}
// 删除拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
原理
C++11 规定,对函数内静态对象的初始化是 线程安全 的。编译器会在第一次调用 instance() 时执行一次初始化,后续调用不再重复。只要编译器遵守标准,这种方式既简单又高效。
优点
- 代码最短,最易读。
- 无需手动同步,避免死锁与竞态。
- 延迟初始化(懒加载),在首次使用时才创建。
缺点
- 需要 C++11 或更高。
- 对析构时的销毁顺序(尤其是多线程结束时)有些微的不确定性。
- 对于跨进程共享单例(如共享内存)无法直接使用。
2. 双重检查锁定(Double‑Checked Locking)
#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_;
原理
- 第一次检查
instance_是否已创建,若未创建则进入锁区。 - 再次检查(再次锁定前)确认实例仍未创建,避免多线程同时创建多份实例。
- 使用
std::atomic与内存序保证可见性。
优点
- 兼容 C++11 之前的编译器(但需要显式同步)。
- 控制细粒度的锁,性能相对良好。
缺点
- 代码较繁琐,易出错。
- 需要手动管理
delete,若程序退出时未释放会造成内存泄漏。 - 需要
std::atomic的正确使用,错误的内存序会导致数据竞争。
3. 静态局部变量与 std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []{
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
原理
std::call_once确保给定的 lambda 只执行一次。std::once_flag负责同步,内部实现使用原子操作和锁。
优点
- 与双重检查锁定相似,但代码更简洁。
- 支持 C++11 及以上。
缺点
- 与
std::call_once的实现细节相关,某些老旧编译器可能不完美。 - 与静态局部变量相比,缺少自动销毁(除非在
atexit注册销毁函数)。
4. 适配多进程环境的单例(共享内存单例)
在多进程共享内存时,单例需要在共享内存段内创建。下面演示一种基于 boost::interprocess 的实现思路(可根据需求替换为 POSIX shm_open/mmap 等):
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
struct Singleton {
Singleton() = default;
// 业务成员...
};
Singleton* getSingleton() {
using namespace boost::interprocess;
static managed_shared_memory segment(open_or_create, "MySharedMemory", 65536);
static interprocess_mutex mutex;
static Singleton* instance = nullptr;
if (!instance) {
boost::interprocess::scoped_lock <interprocess_mutex> lock(mutex);
if (!instance) {
instance = segment.construct <Singleton>("SingletonInstance")();
}
}
return instance;
}
说明
managed_shared_memory在共享内存区创建对象。interprocess_mutex处理进程间同步。- 适用于多进程共享同一实例(如数据库连接池、缓存等)。
5. 何时使用哪种实现?
| 场景 | 推荐实现 | 备注 |
|---|---|---|
| 单进程、C++11+ | Meyers Singleton | 最简洁,线程安全 |
| 旧编译器(C++03) | 双重检查锁定 | 需要手动同步 |
| 需要手动销毁 | std::call_once + 自定义析构 | 兼顾性能与可控性 |
| 多进程共享 | 共享内存 + 进程间同步 | 复杂度更高,需考虑映射、权限 |
6. 常见陷阱与最佳实践
- 删除拷贝构造/赋值:避免被复制产生多个实例。
- 懒加载 vs 预初始化:若实例化成本高且启动阶段不需要,使用懒加载;若想避免启动时的延迟,考虑在程序初始化阶段显式创建。
- 销毁顺序:静态局部对象在程序退出时按逆序销毁;若涉及跨文件的静态单例,需小心析构顺序。
- 异常安全:若构造函数抛异常,静态局部对象会再次尝试初始化,确保异常不导致程序崩溃。
- 多线程测试:在多核机器上使用
std::thread并发访问instance(),验证线程安全。
7. 小结
- C++11 为单例提供了最直接的线程安全实现:静态局部对象。
- 对于更旧的环境或更细粒度的控制,可使用双重检查锁定或
std::call_once。 - 多进程共享单例需要进程间同步与共享内存。
- 关键在于删除拷贝、避免析构顺序问题、保证线程安全。
通过以上方式,你可以根据项目需求和编译环境,选用最合适的单例实现,既保证了线程安全,又保持了代码的可维护性。