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

在多线程环境下,单例模式需要确保只产生一个实例,并且在并发访问时不会出现竞争条件。下面给出一种现代 C++(C++11 及以后)实现方法,并对其工作原理进行详细解析。

1. 经典实现中的问题

传统的单例实现往往使用双重检查锁(double‑checked locking):

class Singleton {
public:
    static Singleton& getInstance() {
        if (!instance) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!instance) instance = new Singleton();
        }
        return *instance;
    }
private:
    Singleton() {}
    static Singleton* instance;
    static std::mutex mtx;
};

虽然在某些平台能正常工作,但由于 内存可见性构造顺序 的问题,标准并不保证其线程安全,特别是在编译器优化层面可能导致实例对象在完全构造之前就被其他线程看到。

2. C++11 的“构造即初始化”方案

从 C++11 开始,函数内部的静态局部变量初始化是 线程安全 的。只需把单例实例放在 getInstance() 的局部静态对象即可:

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& getInstance() {
        static ThreadSafeSingleton instance; // 线程安全初始化
        return instance;
    }

    // 禁止拷贝构造和赋值
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

    void doSomething() { /* 业务逻辑 */ }

private:
    ThreadSafeSingleton() { /* 资源初始化 */ }
    ~ThreadSafeSingleton() { /* 清理工作 */ }
};

关键点:

  • static ThreadSafeSingleton instance; 在第一次调用时初始化,随后所有线程直接使用同一实例。编译器保证在多线程访问时只执行一次初始化。
  • 删除拷贝构造和赋值操作,防止外部复制导致多实例产生。
  • 析构函数可以在程序结束时自动调用,或者手动控制生命周期。

3. 延迟销毁(懒销毁)

如果想让单例在程序退出前保持存在,可以使用 std::shared_ptr 结合 std::weak_ptr

class LazyDestroySingleton {
public:
    static LazyDestroySingleton& getInstance() {
        std::call_once(flag, [](){ ptr.reset(new LazyDestroySingleton); });
        return *ptr;
    }
private:
    LazyDestroySingleton() {}
    ~LazyDestroySingleton() {}
    static std::once_flag flag;
    static std::shared_ptr <LazyDestroySingleton> ptr;
};
std::once_flag LazyDestroySingleton::flag;
std::shared_ptr <LazyDestroySingleton> LazyDestroySingleton::ptr;

std::call_once 确保只创建一次对象,而 shared_ptr 负责自动销毁。需要注意的是,如果存在循环引用,可能导致内存泄漏。

4. 对比与适用场景

实现方式 优点 缺点 适用场景
局部静态对象 简洁、线程安全、标准保证 不能显式控制销毁时机 大多数单例需求
std::call_once + shared_ptr 可显式销毁、延迟释放 代码稍复杂 需要精确生命周期管理的场景

5. 完整示例

#include <iostream>
#include <mutex>

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

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> guard(mtx_);
        std::cout << "[" << id_ << "] " << msg << std::endl;
    }

private:
    Logger() : id_(++counter_) {}
    ~Logger() {}

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

    std::mutex mtx_;
    int id_;
    static int counter_;
};

int Logger::counter_ = 0;

// 多线程演示
#include <thread>
#include <vector>

void worker(int id) {
    Logger::instance().log("Worker " + std::to_string(id) + " started");
    // ... do work ...
    Logger::instance().log("Worker " + std::to_string(id) + " finished");
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
        threads.emplace_back(worker, i);
    for (auto& t : threads) t.join();
    return 0;
}

运行结果表明,所有线程共享同一 Logger 实例,且日志输出互不干扰。

6. 小结

  • 函数内部静态局部变量 是实现线程安全单例的最简洁方法,C++11 标准已保证初始化安全。
  • 若需 显式销毁自定义生命周期,可以结合 std::once_flagstd::shared_ptrstd::unique_ptr
  • 避免使用双重检查锁(双重检查锁不安全),除非你自己实现了所有必要的同步原语。

通过上述方法,你可以在 C++ 程序中安全、轻松地使用单例模式,而不会出现多线程下的竞态条件或未定义行为。

发表评论