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

在多线程环境下,确保单例对象只被创建一次并且可以安全地被所有线程访问是一项常见需求。下面以 C++17 为例,演示几种常用的线程安全单例实现方式,并讨论它们各自的优缺点。


1. C++11 之静态局部变量(Meyers 单例)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;   // C++11 guarantees thread-safe initialization
        return inst;
    }
    // 其他业务接口
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

原理

C++11 对局部静态变量的初始化进行了同步,保证了多线程下第一次进入 instance() 时的构造只会执行一次。后续访问直接返回已构造的对象。

优点

  • 实现简单:无须手动管理锁或原子操作。
  • 高效:构造后访问不需要额外同步。
  • 安全:构造函数可以抛异常,标准会自动处理。

缺点

  • 无法延迟销毁:对象在程序退出时才销毁,若需要显式销毁需手动实现。
  • 不支持按需初始化参数:构造时无法传参。

2. 经典双重检查锁(双重检查锁定)

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {                     // 1. First check
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {                 // 2. Second check
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
    static void destroy() {
        std::lock_guard<std::mutex> lock(mutex_);
        delete instance_;
        instance_ = nullptr;
    }
private:
    Singleton() = default;
    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

原理

  • 第一次检查可避免每次访问都加锁。
  • 第二次检查保证在多线程竞争下只有一个线程真正创建实例。

优点

  • 延迟销毁:可在需要时手动销毁实例。
  • 可传参:构造时可以使用额外参数。

缺点

  • 易出错:需要正确使用 std::atomicmemory_order 以避免重排问题。
  • 性能略低:每次访问仍需一次无锁检查,且在第一次创建时会锁定。

3. std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []{ instance_ = new Singleton(); });
        return *instance_;
    }
    static void destroy() {
        delete instance_;
        instance_ = nullptr;
    }
private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

原理

std::call_once 保证指定的 lambda 只会被调用一次,即使在并发环境下。此方法在 C++11 之后被官方推荐为线程安全单例的实现方式。

优点

  • 实现简洁:无需手动管理锁。
  • 性能好:仅在第一次调用时有同步开销,随后访问无锁。

缺点

  • 同样无法传参:构造时参数无法传递。
  • 销毁手动:需要显式调用 destroy()

4. 智能指针 + 原子

如果你想在单例销毁时更加安全,结合 std::shared_ptrstd::atomic 可以实现:

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::shared_ptr <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 = std::shared_ptr <Singleton>(new Singleton());
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
private:
    Singleton() = default;
    static std::atomic<std::shared_ptr<Singleton>> instance_;
    static std::mutex mutex_;
};

std::atomic<std::shared_ptr<Singleton>> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;

说明

  • 通过 std::shared_ptr 自动管理生命周期,避免显式销毁。
  • 使用原子操作保证指针的可见性。

适用场景

当单例对象需要被多处持有,并且销毁时不想出现悬空指针时,这种方式更为合适。


5. 何时选择哪种实现?

场景 推荐实现 说明
简单单例,生命周期与程序一致 Meyers 单例 代码最简洁
需要显式销毁或传参 双重检查锁 / std::call_once 兼顾灵活性
多线程安全、性能优先 std::call_once C++11 官方推荐
需要共享生命周期 std::shared_ptr + 原子 自动销毁、避免悬空

6. 代码示例:线程安全配置文件读取器

下面给出一个实际项目中常见的单例:配置文件读取器。

#include <fstream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <mutex>
#include <memory>

class Config {
public:
    static Config& instance(const std::string& path = "config.ini") {
        static std::once_flag flag;
        static std::unique_ptr <Config> instance;
        std::call_once(flag, [&]{
            instance.reset(new Config(path));
        });
        return *instance;
    }

    std::string get(const std::string& key, const std::string& default_val = "") const {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = data_.find(key);
        return it != data_.end() ? it->second : default_val;
    }

private:
    Config(const std::string& path) {
        std::ifstream file(path);
        std::string line;
        while (std::getline(file, line)) {
            if (line.empty() || line[0] == '#') continue;
            std::istringstream iss(line);
            std::string key, eq, value;
            if (iss >> key >> eq >> value && eq == "=") {
                data_[key] = value;
            }
        }
    }
    std::unordered_map<std::string, std::string> data_;
    mutable std::mutex mutex_;
};
  • 使用方式
auto& cfg = Config::instance();            // 默认读取 config.ini
auto dbHost = cfg.get("db_host", "localhost");
  • 优点:只在第一次访问时读取文件,后续访问无锁(只对读取操作加锁)。

7. 小结

  • C++11 已经提供了可靠的单例实现方式,推荐使用 static 局部变量或 std::call_once
  • 若需要 显式销毁传参,可考虑双重检查锁或自定义 std::once_flag
  • 对于 复杂生命周期 的对象,结合 std::shared_ptr 与原子可以更安全。
  • 最终选择应根据项目需求、性能要求和代码可维护性综合决定。

祝你在 C++ 单例实现上顺利,代码简洁又安全!

发表评论