在 C++ 开发中,单例模式(Singleton)常被用于实现全局共享资源,例如日志记录器、配置管理器等。随着多线程程序的普及,单例的线程安全实现成为关键点。下面我们将从多种实现方式进行对比,说明它们的优缺点,并给出实战代码示例。
1. 传统懒汉式(带锁)
class Logger {
public:
static Logger& getInstance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new Logger();
}
return *instance_;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger* instance_;
static std::mutex mutex_;
};
Logger* Logger::instance_ = nullptr;
std::mutex Logger::mutex_;
- 优点:实现直观,所有线程都能保证安全。
- 缺点:每次访问都需要获取互斥锁,导致性能下降。且必须手动管理单例生命周期,容易出现内存泄漏。
2. 饿汉式(编译期构造)
class Config {
public:
static Config& getInstance() {
return instance_;
}
private:
Config() = default;
~Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
static Config instance_;
};
Config Config::instance_;
- 优点:无需锁,线程安全;实例在程序启动时就创建。
- 缺点:如果单例使用量不高,导致资源在程序未用到时就被分配,且不可延迟销毁。
3. C++11 std::call_once + std::once_flag
class DBConnection {
public:
static DBConnection& getInstance() {
std::call_once(flag_, [](){ instance_.reset(new DBConnection()); });
return *instance_;
}
private:
DBConnection() = default;
~DBConnection() = default;
DBConnection(const DBConnection&) = delete;
DBConnection& operator=(const DBConnection&) = delete;
static std::unique_ptr <DBConnection> instance_;
static std::once_flag flag_;
};
std::unique_ptr <DBConnection> DBConnection::instance_;
std::once_flag DBConnection::flag_;
- 优点:懒加载、线程安全、代码简洁。
std::call_once确保一次性初始化,后续访问无需锁。 - 缺点:
std::unique_ptr的销毁时机取决于程序结束,若需要在特定时间销毁,需自行控制。
4. Meyer’s 单例(局部静态变量)
class Cache {
public:
static Cache& getInstance() {
static Cache instance; // C++11 之后,初始化是线程安全的
return instance;
}
private:
Cache() = default;
~Cache() = default;
Cache(const Cache&) = delete;
Cache& operator=(const Cache&) = delete;
};
- 优点:实现最简洁,编译器保证线程安全(C++11 之后)。实例在第一次调用
getInstance()时才创建,后续无需锁。 - 缺点:C++11 标准保证线程安全,但在某些极端多线程场景(如多进程或在异常中初始化)仍需注意。
5. 延迟销毁与智能指针
如果单例需要在程序运行期间多次创建/销毁,可结合 std::shared_ptr 与 std::weak_ptr 实现:
class Service {
public:
static std::shared_ptr <Service> getInstance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = std::shared_ptr <Service>(new Service());
}
return instance_;
}
static void release() {
std::lock_guard<std::mutex> lock(mutex_);
instance_.reset();
}
private:
Service() = default;
~Service() = default;
static std::shared_ptr <Service> instance_;
static std::mutex mutex_;
};
std::shared_ptr <Service> Service::instance_;
std::mutex Service::mutex_;
- 优点:可以显式释放单例,适用于资源占用较大的对象。
- 缺点:每次
getInstance()仍需锁,性能稍低。
6. 代码实践:多线程日志记录
#include <iostream>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
#include <chrono>
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // Meyer's 单例
return instance;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
outfile_ << msg << std::endl;
}
private:
Logger() {
outfile_.open("app.log", std::ios::app);
}
~Logger() { outfile_.close(); }
std::ofstream outfile_;
std::mutex mutex_;
};
void worker(int id) {
for (int i = 0; i < 5; ++i) {
Logger::getInstance().log("Thread " + std::to_string(id) + " msg " + std::to_string(i));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}
- 说明:
Logger使用 Meyer’s 单例实现,文件写入在log()内部加锁,确保线程安全。 - 扩展:可以加入日志等级、滚动日志文件等功能。
7. 结语
-
选择何种实现?
- 若需 懒加载 且 性能敏感,推荐
std::call_once或 Meyer’s 单例。 - 若需要 可延迟销毁,使用
std::shared_ptr+std::mutex。 - 对于不需要懒加载的情况,可直接采用 饿汉式。
- 若需 懒加载 且 性能敏感,推荐
-
注意事项
- 线程安全不等于无锁;正确使用
std::mutex、std::lock_guard可以保持安全与简洁。 - 在使用单例时,需评估其生命周期与资源占用,避免单例造成的 资源泄漏 或 延迟销毁 产生的内存压力。
- 线程安全不等于无锁;正确使用
通过上述多种实现方式,开发者可以根据具体需求挑选最合适的单例模式,并在多线程环境中安全高效地共享资源。祝你编码愉快!