如何在C++中实现一个线程安全的单例模式?

单例模式(Singleton)保证一个类只有一个实例,并提供全局访问点。在多线程环境下,若实现不当,可能会产生多实例或资源竞争。下面从多个角度展示在C++中实现线程安全单例的常见手段,并讨论其优缺点。

1. Meyers单例(局部静态变量)

class Logger {
public:
    static Logger& instance() {
        static Logger logger;   // C++11 起线程安全
        return logger;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << msg << std::endl;
    }

private:
    Logger() = default;
    ~Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    std::mutex mutex_;
};
  • 优点:实现最简洁;C++11 之后编译器保证局部静态变量初始化线程安全;延迟实例化,第一次访问时才创建。
  • 缺点:无法在实例销毁前自定义初始化顺序(例如需要先初始化其他全局对象)。

2. std::call_oncestd::once_flag

class Config {
public:
    static Config& getInstance() {
        std::call_once(initFlag_, [](){
            instance_ = new Config(loadFromFile());
        });
        return *instance_;
    }

private:
    explicit Config(const std::map<std::string, std::string>& cfg)
        : data_(cfg) {}

    static Config* instance_;
    static std::once_flag initFlag_;

    std::map<std::string, std::string> data_;
};

Config* Config::instance_ = nullptr;
std::once_flag Config::initFlag_;
  • 优点:对复杂的初始化流程(如读取配置文件、网络请求)可做一次性初始化,且线程安全。
  • 缺点:需要手动管理析构(上例中未显式销毁),如果不想手动释放,可以使用 std::unique_ptrstd::shared_ptr

3. 双重检查锁(Double-Check Locking)

class Database {
public:
    static Database* getInstance() {
        if (!instance_) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) {
                instance_ = new Database();
            }
        }
        return instance_;
    }

private:
    Database() = default;
    static Database* instance_;
    static std::mutex mutex_;
};

Database* Database::instance_ = nullptr;
std::mutex Database::mutex_;
  • 优点:只有第一次访问需要加锁,性能相对更优。
  • 缺点:在C++中实现难度大,尤其是 instance_ 必须是 std::atomic<Database*>,否则可能出现指令重排导致读到未构造对象。

4. 智能指针 + std::shared_ptr

class Service {
public:
    static std::shared_ptr <Service> getInstance() {
        std::call_once(initFlag_, [](){
            instance_ = std::make_shared <Service>(initialize());
        });
        return instance_;
    }

private:
    explicit Service(const Config& cfg) : config_(cfg) {}
    static std::shared_ptr <Service> instance_;
    static std::once_flag initFlag_;
    Config config_;
};

std::shared_ptr <Service> Service::instance_ = nullptr;
std::once_flag Service::initFlag_;
  • 优点:自动销毁,线程安全且避免裸指针。
  • 缺点:每次访问需要获取共享指针,略微增加开销。

5. 对象池式实现(更通用)

template <typename T>
class Singleton {
public:
    template <typename... Args>
    static T& instance(Args&&... args) {
        std::call_once(flag_, [&]{
            ptr_.reset(new T(std::forward <Args>(args)...));
        });
        return *ptr_;
    }

private:
    static std::unique_ptr <T> ptr_;
    static std::once_flag flag_;
};

template <typename T>
std::unique_ptr <T> Singleton<T>::ptr_ = nullptr;

template <typename T>
std::once_flag Singleton <T>::flag_;

使用方式:

class Engine { /* ... */ };
Engine& eng = Singleton <Engine>::instance(/* constructor args */);
  • 优点:复用代码,支持不同类型单例。
  • 缺点:模板实现复杂度略高。

6. 何时选择哪种实现?

场景 推荐实现 说明
仅需延迟实例化,且初始化不依赖外部资源 Meyers 单例 简洁,C++11 线程安全
初始化过程复杂(I/O、网络) std::call_once 能保证一次性初始化
需要自定义销毁顺序 静态对象 + atexitstd::unique_ptr 可在程序退出前释放
想避免全局对象初始化顺序问题 std::shared_ptr + call_once 自动管理生命周期
需要多种类型单例 模板 `Singleton
` 统一接口

7. 结语

在现代 C++(C++11 及以后)里,单例模式实现已大大简化。最常见、最安全的做法是使用局部静态变量(Meyers单例),因为编译器已内置线程安全保证。若有更复杂需求,可以结合 std::call_once 或模板包装来满足。记住:单例不等于“万能”,在高并发或模块化设计中往往更推荐使用依赖注入或服务定位器,以保持代码的可测试性与可维护性。

发表评论