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

在 C++20 之前,单例的实现常常依赖于 Meyers 单例(局部静态变量)或手动双重检查锁定(double-checked locking)。然而,C++20 引入了更强大的并发工具,例如 std::atomicstd::mutexstd::call_once,以及更简洁的语法特性。下面给出一个完整的、线程安全、懒加载、易于使用的单例实现,并对关键点进行解释。

1. 基本思路

  • 懒加载:单例对象仅在第一次访问时才创建,避免无谓的资源占用。
  • 线程安全:在多线程环境下保证只有一个实例被创建,且后续访问直接返回该实例。
  • 简洁易用:使用者仅需通过 Singleton::instance() 获取引用,无需关心线程同步细节。

2. 代码实现

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

class Singleton {
public:
    // 获取单例引用
    static Singleton& instance() {
        // std::call_once 与 std::once_flag 结合,保证只执行一次初始化
        std::call_once(initFlag_, []{
            // 使用 std::unique_ptr 以确保析构时自动销毁
            instancePtr_ = std::unique_ptr <Singleton>(new Singleton());
        });
        return *instancePtr_;
    }

    // 禁止复制与移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    // 示例方法
    void doSomething() {
        std::lock_guard<std::mutex> lock(mtx_);
        std::cout << "Doing something in singleton, thread ID: " << std::this_thread::get_id() << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton constructed, thread ID: " << std::this_thread::get_id() << std::endl;
    }
    ~Singleton() = default;

    static std::once_flag initFlag_;
    static std::unique_ptr <Singleton> instancePtr_;
    std::mutex mtx_;  // 保护成员数据
};

// 静态成员初始化
std::once_flag Singleton::initFlag_;
std::unique_ptr <Singleton> Singleton::instancePtr_;

3. 关键点说明

  1. std::call_oncestd::once_flag

    • std::call_once 确保给定的 lambda 在多线程环境下只被执行一次。
    • std::once_flag 用于标记是否已执行,内部实现已经做了高效的原子操作和锁。
  2. 使用 std::unique_ptr

    • 通过智能指针管理单例生命周期,确保程序退出时自动析构。
    • 也避免了裸指针的悬挂指针风险。
  3. 禁止拷贝与移动

    • 单例必须唯一,拷贝/移动构造/赋值会破坏这一约束。
  4. 线程安全的成员操作

    • 对单例内部需要线程保护的成员使用 std::mutex 或更细粒度的同步机制。
  5. 懒加载

    • instancePtr_ 在第一次调用 instance() 时才会被创建。若不需要使用单例,则不必开辟资源。

4. 使用示例

#include <thread>

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

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

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

    return 0;
}

运行结果示例(线程 ID 可能不同):

Singleton constructed, thread ID: 140123456789120
Doing something in singleton, thread ID: 140123456788064
Doing something in singleton, thread ID: 140123456787008
Doing something in singleton, thread ID: 140123456785952

可以看到,单例构造函数仅被调用一次,随后所有线程共享同一个实例。

5. 性能考量

  • 首次访问开销std::call_once 需要一次轻量级锁判断,几乎无开销。
  • 后续访问开销:直接返回已创建的对象指针,几乎为零。
  • 多线程环境:只有在第一次访问时才会有同步争抢,后续访问不再涉及锁。

6. 进阶:使用 std::atomic 进一步简化

如果单例本身不需要在构造后再初始化其他资源,可以使用 std::atomic<Singleton*> 并配合 std::call_once,避免 std::unique_ptr 的使用。示例:

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []{
            instancePtr_.store(new Singleton(), std::memory_order_release);
        });
        return *instancePtr_.load(std::memory_order_acquire);
    }
    // ...
private:
    static std::once_flag initFlag_;
    static std::atomic<Singleton*> instancePtr_;
};

然而,std::unique_ptr 更安全、更易维护,通常推荐使用。

7. 小结

  • C++20 的并发特性让实现线程安全、懒加载单例变得简单且高效。
  • 关键是 std::call_once + std::once_flag 的组合,配合 std::unique_ptr,即可获得安全且易用的单例。
  • 在实际项目中,可以根据业务需要进一步扩展单例的功能,例如延迟初始化、双重检查锁定等,但不必过度复杂化。

通过上述实现,你可以在任何需要全局唯一对象的场景下安全、简洁地使用 C++20 单例模式。

发表评论