如何在C++中实现线程安全的单例模式?

单例模式(Singleton)是一种常见的设计模式,保证一个类只有一个实例,并提供全局访问点。在多线程环境下,若不做正确处理,可能会导致多个实例被创建,从而破坏单例的本意。下面给出几种在C++中实现线程安全单例的方式,并对每种方案的优缺点进行简要说明。

1. Meyers 单例(局部静态变量)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11起,局部静态变量初始化是线程安全的
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}
};
  • 优点

    • 简单、代码量少。
    • 由于 C++11 标准规定,局部静态变量的初始化是线程安全的,编译器会自动插入必要的同步机制。
    • 延迟加载:实例在第一次调用时才创建。
  • 缺点

    • 需要 C++11 或更高版本。
    • 对于需要提前初始化或在销毁时执行特定操作的场景不够灵活。

2. 双重检查锁(Double-Checked Locking)

#include <mutex>

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

Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
  • 优点

    • 兼容 C++03,适用于旧项目。
    • 只在第一次需要时才加锁,后续访问成本低。
  • 缺点

    • 需要使用 volatilestd::atomic 来保证内存可见性,否则存在重排序导致的未初始化对象泄露风险。
    • 代码较为繁琐,易出错。

3. 静态指针与 std::call_once

#include <mutex>

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

Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
  • 优点

    • 明确的单次初始化语义,易于理解。
    • 对于复杂的初始化过程,可以在 lambda 中执行任何操作。
    • 兼容 C++11 及以上。
  • 缺点

    • 需要手动管理实例的销毁(可以借助 std::unique_ptratexit)。
    • Meyers 单例相比略显冗余。

4. 使用 std::unique_ptr + 静态局部

#include <memory>

class Singleton {
public:
    static Singleton& instance() {
        static std::unique_ptr <Singleton> ptr{new Singleton};
        return *ptr;
    }
private:
    Singleton() {}
};
  • 优点

    • 自动管理内存,避免泄漏。
    • 适用于需要在程序退出时执行析构时做额外清理的情况。
  • 缺点

    • Meyers 方案类似,仍依赖 C++11。

5. 编译器扩展:__declspec(thread)(仅限 MSVC)

class Singleton {
public:
    static Singleton& instance() {
        thread_local Singleton instance; // 每个线程有自己的实例
        return instance;
    }
};
  • 说明
    这不是传统意义上的全局单例,而是线程局部单例(Thread-Local Singleton)。在某些场景下,例如需要每个线程拥有独立实例但又想统一管理资源时,可以使用。

6. 何时选用哪种方案?

场景 推荐方案
需要兼容 C++03 双重检查锁或 std::call_once(自定义实现)
只需简单单例 Meyers 单例(C++11)
需要在初始化前后做复杂操作 std::call_once + lambda
想避免手动 delete std::unique_ptr + 静态局部
需要线程局部单例 thread_local 关键字

7. 小结

线程安全的单例实现并非一成不变,选择合适的方案取决于项目的 C++ 标准、性能需求以及初始化/销毁的复杂度。最常见且最简洁的方式是 Meyers 单例,它利用 C++11 对局部静态变量初始化的线程安全保证,几乎无需额外代码。然而,对于需要兼容旧标准或更细粒度控制的情况,std::call_once 或双重检查锁仍然是可靠的备选方案。请根据项目实际情况进行选型,并结合单元测试验证多线程环境下的正确性。

发表评论