在 C++11 之后,标准库提供了多种实现线程安全单例模式的手段。本文将从语言特性、常见实现方式以及实际应用场景几个角度,系统阐述如何在现代 C++ 中安全地实现单例。
1. 单例模式的基本思路
单例模式要求在整个程序生命周期内,某个类只能有唯一的实例。传统实现往往使用私有构造函数、静态成员指针以及公开的 getInstance() 接口来完成。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 1. 静态局部对象
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
2. 线程安全的关键点
在多线程环境下,最常见的竞态条件是:两条线程同时进入 getInstance(),导致两个不同的 Singleton 实例被创建。为避免此类情况,需要确保实例化过程是原子且可重入的。
2.1 C++11 的静态局部变量初始化
自 C++11 起,局部静态变量的初始化是线程安全的。这意味着上面代码中的 static Singleton instance; 在第一次被访问时会自动被保护,避免了多线程重复初始化。无论多少线程同时调用 getInstance(),编译器会插入必要的锁机制。
2.2 std::call_once 与 std::once_flag
如果你想手动控制初始化,或者需要在构造过程中执行复杂逻辑(例如读取配置文件、连接数据库等),可以使用 std::call_once:
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, [](){
instance.reset(new Singleton());
});
return *instance;
}
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
std::call_once 保证给定 lambda 只会执行一次,即使多线程并发调用也能保持安全。
3. 延迟销毁与 std::shared_ptr
在 C++11 之前,单例往往采用 delete 在程序退出时手动销毁。然而,在多线程环境中,析构顺序问题可能导致未定义行为。使用 std::shared_ptr 并结合 std::weak_ptr 可以让单例对象在最后一次引用失效时自动销毁:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(initFlag, [](){
instance = std::make_shared <Singleton>();
});
return instance;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::shared_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
这样,即使多个线程持有 std::shared_ptr,对象也会在最后一次引用消失时安全析构。
4. 在类内部实现单例(友元技术)
有时你希望单例只在类内部使用,外部无法获取引用。可以将 getInstance() 设为私有,并使用友元类或内部结构访问:
class Singleton {
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
class Accessor {
public:
static Singleton& get() {
static Singleton instance;
return instance;
}
};
};
此时,只有 Accessor 能够访问单例实例,外部无法直接调用。
5. 性能与可见性考虑
- 局部静态变量:首次访问时会有一次锁竞争,之后访问速度与普通局部变量无异。
std::call_once:同样会有一次锁竞争,适用于一次性初始化。若初始化非常昂贵,使用此法可以减少不必要的同步。std::atomic:若你仅需在多线程间保证可见性(不需要同步初始化),可以使用std::atomic<Singleton*>来实现双检锁(double‑checked locking)。但要注意内存模型和可见性,避免出现指针先写后读的情况。
6. 实际案例:日志系统单例
class Logger {
public:
static Logger& instance() {
static Logger inst; // 线程安全
return inst;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "[" << std::chrono::system_clock::now().time_since_epoch().count() << "] " << msg << '\n';
}
private:
Logger() = default;
std::mutex mtx;
};
在多线程环境下,每个线程都可以通过 Logger::instance() 写日志,内部的 mtx 保证输出顺序一致。
7. 总结
- C++11 为单例提供了天然的线程安全机制:局部静态变量和
std::call_once。 - 选择哪种实现方式取决于初始化成本、销毁需求以及是否需要手动控制初始化。
- 在实际项目中,建议使用局部静态变量或
std::call_once,避免手动实现锁机制以减少错误。 - 对于需要延迟销毁的场景,可考虑
std::shared_ptr。
掌握这些技术后,你可以在任何需要全局唯一对象的地方,安全、简洁地实现单例模式。