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

在现代 C++(C++11 及以后)中,最简洁且线程安全的单例实现依赖于局部静态变量的初始化。C++ 标准保证了函数内部局部静态对象的构造是线程安全的,即使多个线程同时进入该函数,编译器会在内部使用锁机制来确保只构造一次。下面给出一个典型实现,并解释其细节、优点与常见误区。

#include <mutex>
#include <iostream>

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(mtx_);
        std::cout << "[" << ++counter_ << "] " << msg << std::endl;
    }

private:
    Logger() : counter_(0) {}        // 私有构造函数
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    std::mutex mtx_;
    size_t counter_;
};

关键点解析

  1. 局部静态对象
    static Logger logger; 只在第一次调用 instance() 时创建。后续调用直接返回已存在的实例。由于 C++11 引入了对局部静态对象初始化的线程安全保证,无需额外同步。

  2. 私有构造与拷贝/赋值删除
    通过私有构造函数阻止外部直接创建实例;删除拷贝构造和赋值运算符避免了实例被复制或重新赋值,确保单例唯一。

  3. 成员同步
    虽然实例创建已线程安全,但使用单例的业务方法仍需考虑并发访问。示例中使用 std::mutex 保护日志输出,避免多线程写入时交错。

常见误区

  • 使用 new 及手动 delete
    传统实现会使用 static Logger* ptr = nullptr; 并在 instance() 内进行 if (!ptr) ptr = new Logger;。这在多线程环境下可能导致竞争条件,除非手动加锁或使用 std::call_once

  • 双重检查锁定(DCL)
    在 C++11 前,双重检查锁定常被用来实现懒加载,但需要显式使用 std::mutex 并对指针进行原子操作。C++11 的局部静态初始化已简化此需求。

  • 单例对象的生命周期
    局部静态对象在程序终止时会被销毁,若在 atexit 时使用,需注意对象已经析构的情况。一般不建议在 atexit 里再次访问单例。

小结

通过利用 C++11 的线程安全局部静态对象初始化,单例实现既简洁又安全。只需确保实例方法的并发访问得到适当同步,即可在多线程程序中安全使用单例。若对性能极度敏感,可考虑使用 std::call_once 或原子指针实现更细粒度的控制,但在大多数场景下,上述实现已足够满足需求。

发表评论