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

在多线程环境下,单例模式需要确保即使多个线程同时访问,仍然只会产生一个实例。C++11 引入了线程安全的局部静态变量初始化,使得实现单例变得简单而可靠。以下是一种常见的实现方式,并对关键点做详细说明。

1. 基础单例实现

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 规定此初始化是线程安全的
        return instance;
    }

    // 禁止复制构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 示例方法
    void doSomething() {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton constructor called." << std::endl;
    }
    ~Singleton() = default;
};

关键点说明

  • static Singleton instance; 在函数内部的局部静态变量。C++11 规定第一次进入时的初始化是原子操作,后续访问会被 std::call_once 机制保护,避免多线程竞争。
  • 删除复制构造和赋值运算符,防止外部拷贝导致多实例。
  • 析构函数默认即可,若需要自定义清理逻辑,可以在 ~Singleton() 中实现。

2. 延迟初始化与自销毁

有时你希望单例在首次使用时才真正创建,并在程序结束后自动销毁。上面的实现已满足此需求。若需更细粒度的控制,可以结合 std::unique_ptrstd::atomic

class LazySingleton {
public:
    static LazySingleton& getInstance() {
        LazySingleton* tmp = instance.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new LazySingleton();
                instance.store(tmp, std::memory_order_release);
                std::atexit(&LazySingleton::destroy);
            }
        }
        return *tmp;
    }

private:
    LazySingleton() { std::cout << "LazySingleton constructed\n"; }
    ~LazySingleton() { std::cout << "LazySingleton destroyed\n"; }

    static void destroy() { delete instance.load(std::memory_order_relaxed); }

    static std::atomic<LazySingleton*> instance;
    static std::mutex mtx;
};

std::atomic<LazySingleton*> LazySingleton::instance{nullptr};
std::mutex LazySingleton::mtx;
  • std::atomic 用于避免在多线程中出现未定义行为。
  • std::atexit 保证在程序正常退出时释放资源。

3. 线程安全的懒汉式实现(双重检查锁定)

如果你更熟悉经典的双重检查锁定(Double-Check Locking,DCL),可以这样实现:

class DCLSingleton {
public:
    static DCLSingleton* getInstance() {
        if (instance == nullptr) {               // 第一次检查
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {           // 第二次检查
                instance = new DCLSingleton();
                std::atexit(&DCLSingleton::destroy);
            }
        }
        return instance;
    }

private:
    DCLSingleton() { std::cout << "DCLSingleton constructed\n"; }
    ~DCLSingleton() { std::cout << "DCLSingleton destroyed\n"; }

    static void destroy() { delete instance; }

    static std::atomic<DCLSingleton*> instance;
    static std::mutex mtx;
};

std::atomic<DCLSingleton*> DCLSingleton::instance{nullptr};
std::mutex DCLSingleton::mtx;

注意:在 C++11 之后,使用局部静态变量的方式更简洁、可靠。DCL 需要确保编译器遵循内存模型,否则仍可能出现可见性问题。

4. 单例中的资源管理

单例往往需要管理全局资源,如数据库连接、日志系统等。推荐将这些资源封装为类成员,并在单例构造时初始化:

class LoggerSingleton {
public:
    static LoggerSingleton& getInstance() {
        static LoggerSingleton instance;
        return instance;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(logMutex);
        std::ofstream out(logFile, std::ios::app);
        out << msg << std::endl;
    }

private:
    LoggerSingleton() : logFile("app.log") { std::cout << "Logger initialized\n"; }
    ~LoggerSingleton() = default;

    std::string logFile;
    std::mutex logMutex;
};
  • logMutex 确保多线程写日志时不会出现内容交叉。
  • 使用 std::ofstream 的 RAII 机制自动关闭文件。

5. 单元测试注意事项

测试单例时需小心状态共享。可以在测试框架中提供 Test Fixture,在 SetUp()/TearDown() 中重置单例状态,或使用 std::unique_ptr 手动销毁实例(如果实现允许)。例如:

TEST(LoggerTest, LogMessage) {
    LoggerSingleton& logger = LoggerSingleton::getInstance();
    logger.log("Test message");

    // 验证日志文件中是否存在该行
}

6. 结语

C++11 及以后版本为线程安全单例提供了最简洁的实现方式:使用局部静态变量即可。若业务对初始化时机或销毁顺序有更严格要求,可结合 std::atomicstd::mutexstd::atexit 进行自定义实现。通过合理封装资源、加锁与 RAII,单例模式既能保持全局唯一,又能在多线程环境中保持稳定与高效。

发表评论