C++中实现线程安全的单例模式:懒汉式与双检锁

在现代 C++ 开发中,单例模式(Singleton)经常用于需要全局唯一实例的场景,例如日志系统、配置管理器或连接池。若单例不保证线程安全,可能导致多线程环境下的竞争条件、数据损坏或程序崩溃。下面我们以 C++17 为例,分别讨论两种常见的线程安全懒汉式实现:std::call_once + std::once_flag(推荐)和“双检锁(Double-Check Locking)”,并给出完整代码示例与关键点说明。


1. std::call_once + std::once_flag(推荐方式)

1.1 方案思路

C++11 引入了 std::once_flagstd::call_once,可在多线程环境中保证函数只被执行一次。单例对象的创建可以放在 std::call_once 回调中,从而避免锁竞争与多次初始化。

1.2 代码实现

#include <iostream>
#include <memory>
#include <mutex>
#include <thread>

class Logger {
public:
    static Logger& instance() {
        std::call_once(initFlag_, []() {
            // 用 make_unique 更安全,C++14 起可直接使用
            instance_ = std::make_unique <Logger>();
        });
        return *instance_;
    }

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

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

    // 禁止拷贝和移动
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    static std::unique_ptr <Logger> instance_;
    static std::once_flag initFlag_;
    std::mutex ioMutex_; // 用于同步打印
};

std::unique_ptr <Logger> Logger::instance_;
std::once_flag Logger::initFlag_;

void worker(int id) {
    Logger::instance().log("Thread " + std::to_string(id) + " started");
    // 模拟工作
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    Logger::instance().log("Thread " + 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;
}

1.3 关键点说明

关键点 说明
std::once_flag 只读数据结构,标识是否已初始化
std::call_once 第一次调用时执行 lambda,后续调用不再执行
std::unique_ptr 防止单例被复制或销毁,自动管理生命周期
std::lock_guard 线程安全的 I/O 操作
std::make_unique 安全、简洁的对象创建

2. 双检锁(Double-Check Locking)

2.1 方案思路

双检锁先检查实例是否已创建,若未创建则进入互斥锁再做检查,最后实例化。它在 C++11 的原子类型 std::atomic 支持后成为可行方案。注意避免出现“对象已构造但未对内存屏障可见”的问题,必须使用 std::atomic<Logger*> 并配合 std::memory_order_acquire/release

2.2 代码实现

#include <iostream>
#include <memory>
#include <atomic>
#include <mutex>
#include <thread>

class Logger {
public:
    static Logger& instance() {
        Logger* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lk(initMutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Logger();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return *tmp;
    }

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

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

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

    static std::atomic<Logger*> instance_;
    static std::mutex initMutex_;
    std::mutex ioMutex_;
};

std::atomic<Logger*> Logger::instance_{nullptr};
std::mutex Logger::initMutex_;

void worker(int id) {
    Logger::instance().log("Thread " + std::to_string(id) + " started");
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Logger::instance().log("Thread " + std::to_string(id) + " finished");
}

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

2.3 关键点说明

关键点 说明
std::atomic<Logger*> 原子指针,避免多线程竞争
memory_order_acquire/release 确保构造完成后对所有线程可见
std::lock_guard<std::mutex> 保护初始化区块
nullptr 初始化 采用原子 nullptr 作为未初始化标志

3. 对比与最佳实践

方案 优点 缺点 推荐情况
call_once 简单、安全、无显式锁竞争 仅在 C++11 之后可用 推荐
双检锁 可以手动控制锁粒度 代码更复杂,易错 若需自定义内存管理、对象生命周期时可用
  • 如果项目已使用 C++11/14/17,首选 std::call_once
  • 如果想手动控制实例销毁顺序,可考虑使用 std::shared_ptrstd::unique_ptrstd::call_once 的组合。

4. 进一步扩展

  1. 延迟销毁
    单例在程序退出时不一定立即销毁。可以使用 std::atexit 注册销毁函数,或让 std::unique_ptr 的析构在 main 结束时自动调用。

  2. 多线程初始化性能
    对于极高并发初始化,std::call_once 依旧是最优选择。其实现通常使用 std::once_flag 与内部 mutex,在多线程环境下开销极小。

  3. 线程安全的懒加载与惰性求值
    C++17 的 std::optionalstd::shared_future 可以与单例结合,进一步提升灵活性。


5. 小结

线程安全的单例在 C++ 开发中至关重要。通过 std::call_oncestd::once_flag 的组合,既能保证一次性初始化,又能避免显式锁竞争;双检锁则为更细粒度控制提供了可能,但实现更复杂。熟练掌握这两种技术,能够在多线程项目中稳健地使用单例,提升代码质量与运行时安全。

发表评论