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

在现代 C++(尤其是 C++17 之后)中,实现线程安全且惰性初始化的单例模式已经不再需要复杂的锁机制。标准库提供的 std::call_oncestd::once_flag 以及 constexpr 初始化器,结合 std::unique_ptrstd::shared_ptr,可以让代码既简洁又高效。下面将从几个关键点展开说明,并给出完整可编译的示例。


1. 单例的核心需求

  • 全局唯一实例:保证同一进程内只能有一个对象实例。
  • 懒加载:第一次访问时才创建实例,避免不必要的资源占用。
  • 线程安全:多线程环境下,实例化过程不会出现竞态条件。
  • 易于使用:调用者不需要关心底层实现细节。

2. C++20 里最简洁的实现方式

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

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

    // 提供全局访问入口
    static Singleton& instance()
    {
        // std::call_once 保证只有一个线程会执行初始化代码
        std::call_once(initFlag_, [] {
            // 这里使用 make_unique,构造函数默认调用
            instance_.reset(new Singleton);
        });
        return *instance_;
    }

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

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

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

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

关键点说明

  1. std::once_flag + std::call_once
    这对组合是实现单例懒加载的最标准方式。call_once 会在多线程环境下确保内部 lambda 只被执行一次,无论多少线程同时调用。

  2. std::unique_ptr
    用于管理单例对象的生命周期,保证在程序结束时自动销毁。相比裸指针,避免了内存泄漏。

  3. 删除拷贝构造与赋值
    防止外部误用导致多实例。

  4. 线程 ID 输出
    sayHello 中打印线程 ID,便于验证多线程访问的安全性。


3. 如何使用

void worker()
{
    Singleton::instance().sayHello();
}

int main()
{
    std::thread t1(worker);
    std::thread t2(worker);
    std::thread t3(worker);

    t1.join(); t2.join(); t3.join();

    // 程序结束时,单例会自动析构
    return 0;
}

运行结果类似:

Singleton constructed
Hello from Singleton! Thread ID: 140353219892288
Hello from Singleton! Thread ID: 140353211499584
Hello from Singleton! Thread ID: 140353203106880
Singleton destroyed

可以看到,构造函数只被调用一次,且所有线程都共享同一个实例。


4. 常见误区与注意事项

误区 正确做法
直接使用静态局部变量实现单例 仍然可行,但 call_once 更显式,易于阅读。
main 里手动销毁单例 unique_ptr 自动析构,避免手动调用可能导致顺序错误。
忽略拷贝/移动构造 通过 delete 明确禁止,以防止意外拷贝。

5. 小结

在 C++20 及以上版本中,借助 std::call_oncestd::once_flag 与智能指针,可以用极简的代码实现线程安全、懒加载的单例模式。与传统的 static 局部变量或双重检查锁模式相比,现代实现更易读、易维护,且不需要手动处理锁或计数器。只需几行代码即可满足大多数项目的需求。

发表评论