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

在 C++ 中实现单例(Singleton)模式是为了确保一个类只有一个实例,并提供全局访问点。对于多线程环境,最关键的是保证单例在并发访问时不会被多次实例化。下面提供几种常用且线程安全的实现方法,并说明其优缺点。


1. Meyer’s Singleton(局部静态变量)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 之后的编译器保证线程安全
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() { /* 初始化代码 */ }
    ~Singleton() {}
};
  • 优点

    • 简单、易读。
    • 延迟初始化:只有第一次访问时才会实例化。
    • C++11 标准保证局部静态变量初始化线程安全。
  • 缺点

    • 在某些老旧编译器(C++03)或不符合标准的实现中不一定线程安全。
    • 无法在实例化前进行自定义错误处理或日志。

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

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instance_;
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_;
            if (!tmp) {
                tmp = new Singleton();
                instance_ = tmp;
            }
        }
        return tmp;
    }
    // 其余成员同上

private:
    Singleton() {}
    ~Singleton() {}
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
  • 优点

    • 兼容 C++03,适用于不支持局部静态变量线程安全的编译器。
    • 只在第一次初始化时加锁,后续访问无需锁,性能优越。
  • 缺点

    • 代码复杂,易出错。
    • 需要使用 std::atomicstd::mutex,若不小心会出现“指令重排序”导致线程安全问题。

3. 静态 std::shared_ptrstd::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instancePtr_ = std::shared_ptr <Singleton>(new Singleton());
        });
        return *instancePtr_;
    }
    // 其余成员同上

private:
    Singleton() {}
    ~Singleton() {}

    static std::shared_ptr <Singleton> instancePtr_;
    static std::once_flag initFlag_;
};

std::shared_ptr <Singleton> Singleton::instancePtr_{nullptr};
std::once_flag Singleton::initFlag_;
  • 优点

    • 采用 std::call_once 保证初始化只执行一次,线程安全。
    • std::shared_ptr 方便实现自毁(若需要在程序退出时释放资源)。
    • 代码结构清晰、易于维护。
  • 缺点

    • 需要 C++11。
    • 若想在程序退出时显式销毁实例,需要额外的控制逻辑。

4. 模板实现(更灵活)

如果你想在多处使用相同模式,可以用模板封装:

template <typename T>
class Singleton {
public:
    static T& instance() {
        static T inst;
        return inst;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
protected:
    Singleton() = default;
    ~Singleton() = default;
};

然后只需:

class MyService : public Singleton <MyService> {
    friend class Singleton <MyService>; // 允许基类访问构造函数
    MyService() { /* ... */ }
};

小结

  • 推荐使用:如果使用 C++11 或更高版本,优先使用 Meyer’s Singleton,简单且线程安全。
  • 兼容旧标准:若必须在 C++03 环境下工作,可选择双重检查锁或 std::call_once(需自实现)。
  • 可扩展性:模板实现可在多个类中复用单例机制。

在实际项目中,请根据编译器、项目需求和团队技术栈选择合适的实现方式,并结合单元测试验证线程安全性。

发表评论