在 C++17 及更高版本中,单例模式的实现可以非常简洁且天然线程安全。下面给出一种推荐的实现方式,并解释其线程安全性以及潜在的优化点。
1. 只需要一个静态局部对象
class Singleton {
public:
// 禁止复制和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton& instance() {
static Singleton instance; // C++11 起线程安全的初始化
return instance;
}
void do_something() {
// 业务代码
}
private:
Singleton() = default; // 私有构造
~Singleton() = default;
};
为什么这段代码线程安全?
-
静态局部变量初始化:从 C++11 起,编译器保证对静态局部变量的初始化是 线程安全 的。若多个线程同时调用
Singleton::instance(),编译器会使用内部锁确保只会有一次真正的构造过程,并且在所有线程看到对象之前,该对象已经完全初始化。 -
只读访问:所有线程获取到的
Singleton&引用后,只能通过do_something()等方法访问内部数据。如果do_something()本身不是线程安全的,则需要在方法内部添加同步机制(如std::mutex)。
2. 延迟销毁(可选)
C++11 对象的销毁时机不确定,可能在程序退出时不被调用。若你需要显式销毁(例如释放资源),可以使用 std::unique_ptr 与 std::weak_ptr 的组合:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::shared_ptr <Singleton> ptr(new Singleton, [](Singleton* p){ delete p; });
return ptr;
}
// ...
};
这里 std::shared_ptr 会在程序结束时自动销毁对象,且使用自定义 deleter 可以做更细粒度的资源释放。
3. 性能优化:无锁访问(读多写少)
如果单例对象在大多数时间只读,写入很少,可以采用 std::atomic<std::shared_ptr<Singleton>> 或 std::shared_mutex 的组合,以减少锁竞争。示例:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
void update_config(const Config& cfg) {
std::unique_lock lock(mutex_);
config_ = cfg;
}
Config get_config() const {
std::shared_lock lock(mutex_);
return config_;
}
private:
mutable std::shared_mutex mutex_;
Config config_;
};
std::shared_mutex允许多个读线程同时访问,而写线程则独占。- 只在真正需要修改时获取写锁,读操作几乎无锁。
4. 常见坑点
- 双重检查锁定(Double-Checked Locking):在 C++11 之前,这种模式因为内存可见性问题而不安全。
- 构造函数抛异常:如果构造函数抛出异常,
static变量的初始化会被标记为失败,下次再尝试时会重新抛出。 - 全局析构顺序:如果单例依赖其他全局对象,可能在析构时产生依赖错误。使用
std::atexit或std::shared_ptr解决。
5. 小结
- 在 C++17 及以上版本中,最简单、最安全的单例实现就是使用
static局部变量。 - 对于需要显式销毁或更细粒度的线程同步,可结合
std::shared_ptr、std::shared_mutex等工具。 - 关注异常安全和资源释放顺序,避免全局析构问题。
通过以上技巧,你可以在现代 C++ 中轻松、安全、高效地实现单例模式。