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

单例模式保证一个类只有一个实例,并提供全局访问点。传统实现依赖静态局部变量或静态成员,但在多线程环境下若未做同步处理,可能出现“双重检查锁定”问题或多线程竞争导致实例被多次创建。C++11 之后,语言标准本身对静态局部变量的初始化做了线程安全保证,最简洁且无锁实现的方式就是使用 static 局部变量。以下演示几种实现方式,并说明其线程安全性与使用场景。

1. C++11 线程安全的静态局部变量实现

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 第一次调用时初始化,后续调用直接返回
        return instance;
    }

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 为什么安全?
    C++11 标准规定,静态局部变量在第一次使用时会被初始化,并且该初始化过程是线程安全的。编译器会在内部插入互斥锁,保证同一时刻只有一个线程完成初始化,其他线程会等待。

  • 使用示例

    int main() {
        Singleton& s1 = Singleton::getInstance();
        Singleton& s2 = Singleton::getInstance();
        assert(&s1 == &s2);  // 确保同一实例
    }

2. 显式加锁的 Meyers 单例(兼容旧标准)

如果项目仍使用 C++03 或更早版本,或需在多线程环境下显式控制锁,下面是加锁实现:

#include <mutex>

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

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

private:
    Singleton() = default;
    ~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_oncestd::once_flag 只在第一次调用时执行一次初始化,后续调用无额外开销。
  • 适用于需要在运行时确定是否需要创建实例或需要延迟初始化的场景。

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

class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance) {                      // ①
            std::lock_guard<std::mutex> lock(mutex);
            if (!instance) {                  // ②
                instance = new Singleton;
            }
        }
        return instance;
    }

    // ... 其它成员 ...
private:
    static Singleton* instance;
    static std::mutex mutex;
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
  • 缺点
    • 需要手动管理实例的销毁(如使用 std::atexit 或智能指针)。
    • 对于 C++11 及以上,std::call_once 更简洁、安全。
    • 在某些编译器上,DCL 仍可能出现指令重排导致的可见性问题。

4. 线程局部存储(TLS)实现

如果单例需要在每个线程内独立存在,可以使用 thread_local

class ThreadSingleton {
public:
    static ThreadSingleton& getInstance() {
        thread_local ThreadSingleton instance;
        return instance;
    }
    // ...
};
  • 每个线程拥有自己的实例,适合需要线程隔离资源的场景。

5. 何时使用单例?

  • 全局配置:例如日志系统、线程池、数据库连接池等。
  • 硬件或资源限制:某些硬件资源只能实例化一次,如硬件加速引擎。
  • 不想在全局变量中暴露状态:单例提供受控访问。

6. 单例的陷阱与注意事项

  1. 测试难度:单例全局状态可能导致单元测试之间产生隐式依赖。建议提供 reset() 方法用于测试或使用依赖注入。
  2. 内存泄漏:若不使用智能指针,手动管理 new / delete 可能导致泄漏。C++11 call_once 方式不需要手动销毁,程序退出时会自动释放。
  3. 过度使用:单例并不是万能方案,过度使用会导致代码耦合度提升,维护困难。

结语

在 C++11 及以后,推荐使用静态局部变量实现线程安全的单例。它简洁、无锁、易于维护。若项目受限于旧标准,可采用 std::call_once。掌握这些实现方式后,开发者可以根据具体需求和项目兼容性,选择最合适的单例模式。

发表评论