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

在现代C++中,实现一个线程安全且懒加载(即仅在第一次使用时才创建实例)的单例模式最推荐的方法是利用函数静态局部变量的特性。自C++11起,编译器保证对函数内部静态变量的初始化是线程安全的。下面给出完整示例,并对比几种常见实现方式,帮助你了解它们的优缺点。

1. 基础懒加载单例(线程安全)

#include <iostream>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 起保证线程安全
        return instance;
    }

    void doSomething() { std::cout << "Hello from Singleton\n"; }

private:
    Singleton()   { std::cout << "Constructing Singleton\n"; }
    ~Singleton()  { std::cout << "Destructing Singleton\n"; }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

使用方式:

int main() {
    Singleton::instance().doSomething();
    Singleton::instance().doSomething(); // 只创建一次
}

为什么安全?
函数内部的静态对象在第一次调用时才会被构造。C++11 标准规定,对同一静态对象的并发访问会自动加锁,确保只有一个线程能够完成初始化,其他线程会等待,随后直接获得已经初始化好的对象。

2. 传统 std::call_once 实现

如果你想在更旧的编译器或更细粒度地控制初始化顺序,可以使用 std::call_once

#include <iostream>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, [](){ instancePtr.reset(new Singleton); });
        return *instancePtr;
    }

    void doSomething() { std::cout << "Hello from Singleton\n"; }

private:
    Singleton()   { std::cout << "Constructing Singleton\n"; }
    ~Singleton()  { std::cout << "Destructing Singleton\n"; }

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

    static std::unique_ptr <Singleton> instancePtr;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

此实现与上面完全等价,但在某些极端场景下可以更清晰地表达“只初始化一次”。

3. 双重检查锁(不推荐)

class Singleton {
public:
    static Singleton* instance() {
        if (!ptr) {                       // 第一层检查
            std::lock_guard<std::mutex> lock(mtx);
            if (!ptr) {                   // 第二层检查
                ptr = new Singleton();
            }
        }
        return ptr;
    }
private:
    Singleton() {}
    static Singleton* ptr;
    static std::mutex mtx;
};

虽然逻辑上正确,但在某些编译器和平台上会因为内存模型导致指针可见性问题。现代 C++ 的静态局部变量或 std::call_once 已经解决了这个问题,双重检查锁已经不再推荐。

4. 用 std::shared_ptr 进行懒加载

如果你希望单例在程序结束时自动销毁,而不受函数返回顺序影响,可以使用 std::shared_ptr

static std::shared_ptr <Singleton> instancePtr;
static std::once_flag initFlag;

static std::shared_ptr <Singleton> instance() {
    std::call_once(initFlag, [](){
        instancePtr = std::make_shared <Singleton>();
    });
    return instancePtr;
}

std::shared_ptr 还能让你在需要时获得引用计数,避免手动管理对象生命周期。

5. 单例的常见误区

误区 正确做法
new 并手动 delete 使用局部静态对象或智能指针,避免手动销毁
只关心线程安全,忽略销毁顺序 静态局部变量在程序结束时按逆序销毁,保证资源释放
认为 Meyers Singleton(函数静态)不安全 C++11 之后已保证线程安全
通过宏或全局变量实现 宏会导致名字冲突,建议使用类封装

6. 小结

  • 推荐:使用函数内部静态局部变量(Meyers Singleton),因为代码最简洁,且 C++11 起已保证线程安全。
  • 备选:若需要更细粒度的控制或兼容老编译器,使用 std::call_once
  • 避免:双重检查锁,手动 new/delete,宏实现。

单例模式是 C++ 中经常被讨论的设计模式之一,但在实际项目中,建议先评估是否真的需要全局共享实例。若仅是想共享某个资源,考虑使用依赖注入或模块化设计,以保持代码的可测试性和可维护性。

发表评论