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

在多线程环境下,单例模式常常会遇到并发访问导致的多实例创建问题。传统的懒汉式单例实现虽然实现简单,但在多线程情况下仍有可能出现“竞态条件”。下面给出一种基于 C++17 的线程安全单例实现,兼顾延迟初始化和性能优化,并提供使用示例。

1. 需求分析

  • 延迟加载:只有在第一次使用时才创建实例。
  • 线程安全:多线程并发访问时保证仅创建一次实例。
  • 可扩展:后续需要向单例中添加依赖或进行参数化时也能方便修改。

2. 解决方案

2.1 使用 std::call_oncestd::once_flag

std::call_once 是 C++11 引入的同步机制,确保在多线程环境中仅执行一次给定函数。配合 std::once_flag,可以实现最优的线程安全单例。

#include <mutex>
#include <memory>

class Singleton {
public:
    // 禁止拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    // 获取单例实例
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instancePtr_ = new Singleton();
        });
        return *instancePtr_;
    }

    // 示例方法
    void doSomething() {
        // ...
    }

private:
    Singleton() {
        // 构造时做必要初始化
    }

    ~Singleton() {
        // 销毁时清理资源
    }

    static std::once_flag initFlag_;
    static Singleton* instancePtr_;
};

// 静态成员定义
std::once_flag Singleton::initFlag_;
Singleton* Singleton::instancePtr_ = nullptr;

说明:

  • instance() 采用惰性初始化,第一次调用时才创建实例。
  • std::call_once 内部会使用一次性锁,保证线程安全且只执行一次。
  • instancePtr_ 是裸指针,适用于单例生命周期与程序生命周期相同的场景;如果想让对象在 main 结束时自动销毁,可改为 std::unique_ptrstd::shared_ptr

2.2 采用局部静态变量(C++11+)

C++11 之后,函数内的局部静态变量初始化是线程安全的。相比 std::call_once,实现更简洁,且编译器可做更好的优化。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 保证线程安全
        return instance;
    }

    void doSomething() { /* ... */ }

private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点:

  • 代码更简洁。
  • 线程安全由编译器保证。
  • 延迟初始化且只在第一次访问时创建。

缺点:

  • 对构造函数的异常抛出需要额外处理(若构造抛异常,下一次调用会再次尝试构造,直到成功)。

2.3 需要传递参数的单例

若单例需要在第一次创建时传递参数,可使用工厂方法结合 std::call_once

class ConfigurableSingleton {
public:
    static ConfigurableSingleton& instance(const std::string& config) {
        std::call_once(initFlag_, [&]() {
            instancePtr_ = new ConfigurableSingleton(config);
        });
        return *instancePtr_;
    }

private:
    ConfigurableSingleton(const std::string& cfg) : config_(cfg) {}
    std::string config_;
    static std::once_flag initFlag_;
    static ConfigurableSingleton* instancePtr_;
};

后续调用可以忽略参数:

auto& obj = ConfigurableSingleton::instance(); // 参数可留空

3. 性能与安全性评估

方法 线程安全 延迟初始化 成本 备注
std::call_once 轻量级锁 兼容 C++11+
局部静态变量 无锁 需处理异常
双重检查锁(经典实现) ❌(实现错误常见) 可能导致数据竞争 建议避免

在大多数现代 C++ 代码库中,推荐使用局部静态变量std::call_once两种方案。局部静态变量更简洁,std::call_once 更适合需要传递参数或更复杂初始化流程的场景。

4. 常见陷阱

  1. 析构顺序问题:如果单例对象在全局析构时仍被访问,可能导致悬空引用。可采用 Meyers Singleton(局部静态)或使用 std::unique_ptr 并在 atexit 注册销毁函数。
  2. 跨动态库(DLL)单例:不同模块可能各自加载一次单例,导致多实例。解决方案是将单例放入公共动态库,或使用全局 extern "C" 函数。
  3. 异常安全:在构造函数抛异常时,后续访问会再次尝试初始化,导致可能的无限循环。可在构造函数中捕获并处理异常,或使用 std::unique_ptr 结合 std::make_unique

5. 代码示例:完整实现

#include <iostream>
#include <mutex>
#include <string>

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

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

private:
    Logger() : initialized_(false) {
        // 模拟昂贵初始化
        std::cout << "Logger initializing..." << std::endl;
        initialized_ = true;
    }
    ~Logger() { std::cout << "Logger destroyed." << std::endl; }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    bool initialized_;
    std::mutex mutex_;
};

int main() {
    Logger::getInstance().log("Hello, singleton!");
    return 0;
}

运行结果:

Logger initializing...
[LOG] Hello, singleton!
Logger destroyed.

以上就是在 C++ 中实现线程安全单例模式的常见方案及其细节。根据具体项目需求选择合适的实现方式,即可保证在多线程环境下单例的安全性与性能。

发表评论