Thread‑Safe Singleton Implementation in Modern C++ Using `std::call_once`


在 C++11 之后,标准库为多线程编程提供了诸多工具,其中最常用的是 std::call_oncestd::once_flag
利用它们可以轻松实现 线程安全的单例,而不必担心竞争条件或双重检查锁定(Double‑Checked Locking)带来的陷阱。

下面给出一个完整示例,说明如何:

  1. 使用 std::call_oncestd::once_flag 延迟初始化单例。
  2. 在 C++17/20 时代通过 inline 静态成员或 constexpr 构造函数进一步简化。
  3. 讨论懒加载(Lazy Loading)与饿汉模式(Eager Initialization)的权衡。

1. 基础实现

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

class Singleton {
public:
    // 公开获取实例的静态成员函数
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instance_.reset(new Singleton());
        });
        return *instance_;
    }

    // 演示用的成员函数
    void do_something() const {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

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

private:
    Singleton() { std::cout << "Singleton constructed\n"; }
    ~Singleton() = default;

    static std::unique_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

// 静态成员定义
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;

关键点

  • std::call_once 保证传入的 lambda 只会被执行一次,即使在多线程环境下。
  • std::once_flag 是线程安全的同步原语,避免了传统的互斥锁。
  • 通过 unique_ptr 管理实例,避免手动 delete
  • 删除拷贝构造函数和赋值运算符,防止复制单例。

2. C++17 版本:inline 静态成员

C++17 引入了 inline 静态成员变量,允许在类内部直接初始化。这样可以进一步简化代码:

class Singleton {
public:
    static Singleton& instance() {
        // 这里不需要 std::call_once,因为 C++17 的局部静态变量是线程安全的
        static Singleton instance;   // 线程安全且惰性初始化
        return instance;
    }

    void do_something() const {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

private:
    Singleton() { std::cout << "Singleton constructed\n"; }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点:代码更简洁、无外部同步变量。
缺点:在非常旧的编译器或未支持 C++17 的环境下不可用。

3. C++20 版本:std::atomicstd::optional

C++20 为 std::optional 提供了原子化访问,可以写出更现代的单例:

#include <optional>
#include <atomic>

class Singleton {
public:
    static Singleton& instance() {
        static std::optional <Singleton> opt;
        static std::atomic <bool> initialized{false};

        if (!initialized.load(std::memory_order_acquire)) {
            std::call_once(initFlag_, []() {
                opt.emplace();
                initialized.store(true, std::memory_order_release);
            });
        }
        return *opt;
    }

private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static std::once_flag initFlag_;
};

std::once_flag Singleton::initFlag_;

4. 饿汉与懒汉的比较

方案 初始化时机 线程安全性 资源占用 典型用途
饿汉(静态全局对象) 程序启动时 取决于编译器,通常安全 立即分配 对象不需要延迟,且初始化简单
懒汉std::call_once 或局部静态) 第一次使用时 自动保证线程安全 只在需要时分配 需要延迟或昂贵的初始化

5. 常见误区

  1. 错误的双重检查锁定(Double‑Check Locking)

    if (!ptr) {
        std::lock_guard<std::mutex> lock(mtx);
        if (!ptr) ptr = new Singleton(); // 依赖于内存屏障
    }

    这在 C++ 之前的编译器中不可行;现在推荐直接使用 std::call_once 或局部静态变量。

  2. 使用 new 而不释放
    单例往往是应用程序生命周期内存在的,但如果你在多线程环境中手动 new 并在程序结束时忘记 delete,可能导致资源泄漏。建议使用智能指针或局部静态。

  3. 忽视构造函数抛异常
    如果单例的构造函数抛异常,std::call_once 会把异常重新抛给调用者;随后再次调用 instance() 会重新尝试初始化。

6. 小结

  • std::call_once + std::once_flag 是最通用且安全的实现方式,兼容 C++11 及以后版本。
  • 对于 C++17/20,可以直接使用 局部静态变量std::optional + std::atomic,代码更简洁。
  • 在多线程环境下,永远不要手写锁来实现单例,除非你充分理解内存模型。
  • 了解 饿汉懒汉 的优缺点,选择最适合你项目需求的实现方式。

希望这篇文章能帮助你在 C++ 现代代码中安全、简洁地实现单例模式。祝编码愉快!

发表评论