**题目:C++17 中使用 std::call_once 实现线程安全的单例模式**

在多线程环境下,传统的单例实现常常需要手动加锁或使用双重检查锁定(double-checked locking)来保证线程安全。C++11 及其之后的标准为此提供了更简单、更安全的工具——std::call_oncestd::once_flag。本文将演示如何利用这两者在 C++17 中实现一个懒加载、线程安全且高效的单例类,并讨论其优缺点。


1. 需求与目标

  • 懒初始化:单例对象在第一次使用时才创建,避免程序启动时不必要的开销。
  • 线程安全:在多线程同时访问时只创建一次实例。
  • 高效:在后续调用中不需要再进行锁竞争。

2. 核心工具

组件 作用 典型用法
std::once_flag 记录一次性调用的状态 std::once_flag flag;
std::call_once 只执行一次指定函数 std::call_once(flag, []{ /* 初始化 */ });

std::call_once 的实现保证即使有多个线程同时调用,它也会让其中一个线程执行提供的 lambda(或函数),其余线程会等待,直到该 lambda 执行完毕。此时 once_flag 的状态被标记为已完成,后续对同一 flag 的调用将立即返回。


3. 代码实现

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

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(init_flag_, []() {
            // 延迟初始化
            instance_ptr_ = std::unique_ptr <Singleton>(new Singleton());
        });
        return *instance_ptr_;
    }

    void sayHello() const {
        std::cout << "Hello from Singleton! Thread ID: " << std::this_thread::get_id() << '\n';
    }

private:
    Singleton() {
        std::cout << "Singleton constructed in thread " << std::this_thread::get_id() << '\n';
    }

    static std::once_flag init_flag_;
    static std::unique_ptr <Singleton> instance_ptr_;
};

// 静态成员定义
std::once_flag Singleton::init_flag_;
std::unique_ptr <Singleton> Singleton::instance_ptr_;

int main() {
    // 让 10 个线程同时请求单例
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([]{
            Singleton::instance().sayHello();
        });
    }
    for (auto& t : threads) t.join();
    return 0;
}

运行结果(示例):

Singleton constructed in thread 140245876023296
Hello from Singleton! Thread ID: 140245876023296
Hello from Singleton! Thread ID: 140245867630592
Hello from Singleton! Thread ID: 140245859237888
...

可见,Singleton 只被实例化一次,所有线程共享同一个对象。


4. 关键细节说明

  1. std::unique_ptr 用于持有实例
    通过 std::unique_ptr 可以避免手动 delete,并且在程序结束时自动销毁。

  2. once_flag 必须是静态
    只有静态存储期的对象才会在多线程环境中保证同一实例。once_flag 的生命周期必须覆盖整个程序。

  3. 异常安全
    如果 lambda 中抛出异常,std::call_once 会在该线程中记录异常,并在后续调用时重新抛出。这样可以防止因异常导致单例未正确初始化的情况。

  4. 懒加载与销毁顺序
    std::unique_ptr 在程序结束时按逆序析构,如果你需要自定义销毁顺序,可以考虑使用 std::shared_ptr 与自定义删除器。


5. 与传统实现对比

方案 线程安全 懒加载 代码复杂度 运行时开销
双重检查锁定(DCL) 需要手动加锁,易出错 高(锁竞争)
std::call_once 原生线程安全 低(锁实现内部优化)
静态局部变量 依赖编译器实现 否(即时)

std::call_once 在现代编译器中通常会使用最小化锁策略(如二进制树锁),比手动 std::mutex 更高效。


6. 进阶话题

  • 单例销毁
    如果你想在程序结束前显式销毁单例,可以提供一个 destroy() 成员,并在调用后置空 instance_ptr_。但要注意后续再次访问 instance() 时会重新创建。

  • 多层次单例
    有时需要在不同命名空间下维护各自的单例。可以将 once_flagunique_ptr 放在对应的命名空间或类中。

  • C++20 的 std::atomic<std::shared_ptr>
    对于需要多线程共享但不需要严格一次性初始化的场景,可以使用原子化的共享指针来实现。


7. 小结

  • std::call_oncestd::once_flag 为实现线程安全单例提供了简洁且高效的方式。
  • 只需要一次 std::call_once 调用即可保证单例只被创建一次,后续访问不再需要锁竞争。
  • 代码更易维护,异常安全性也得到提升。

希望本文能帮助你在 C++ 项目中正确、优雅地实现线程安全单例。祝编码愉快!

发表评论