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

单例模式(Singleton)保证一个类只有一个实例,并提供全局访问点。在多线程环境下实现线程安全的单例,需要避免竞争条件和保证实例初始化的原子性。下面从 C++11 开始,逐步介绍几种常见实现方式,并讨论它们的优缺点与使用场景。

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

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

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

private:
    Singleton() {}                // 私有构造函数
    ~Singleton() {}
};
  • 优点

    • 代码最简洁,使用 static 局部变量的初始化在 C++11 之后已保证线程安全。
    • 延迟加载(首次调用时才创建实例)。
    • 无需手动销毁,程序退出时系统自动释放。
  • 缺点

    • 如果实例需要在程序退出前手动销毁(例如依赖顺序),可能需要更复杂的手段。
    • 对于极其早期的 C++ 标准(C++03 或之前),需要手动实现线程同步。

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

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

    // 其他同上
private:
    Singleton() {}
    static Singleton* instance;
    static std::mutex mtx;
};

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

    • 兼容 C++03,适用于老旧编译器。
    • 第一次调用后,后续访问不再加锁,性能好。
  • 缺点

    • 需要确保 instance 的写入对所有线程可见,常用 std::atomic<Singleton*>std::once_flag 替代手动锁。
    • 实现易错,维护成本高。

3. std::call_oncestd::once_flag

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

    // 其他同上
private:
    Singleton() {}
    static Singleton* instance;
    static std::once_flag initFlag;
};

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

    • 更直观的单次初始化语义,编译器实现保证线程安全。
    • 适用于 C++11 及以后,兼容 Meyers 单例实现。
  • 缺点

    • 需要手动删除 instance,如果在多线程环境中释放资源,仍需同步。

4. std::shared_ptrstd::weak_ptr 组合

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (auto ptr = instance.lock()) {   // 先尝试获取已存在实例
            return ptr;
        }
        instance = std::shared_ptr <Singleton>(new Singleton());
        return instance;
    }

    // 其他同上
private:
    Singleton() {}
    static std::weak_ptr <Singleton> instance;
    static std::mutex mtx;
};

std::weak_ptr <Singleton> Singleton::instance;
std::mutex Singleton::mtx;
  • 优点

    • 通过 shared_ptr 自动管理生命周期,避免手动 delete
    • weak_ptr 让单例可以在所有引用失效后被销毁,适用于需要在多次使用后释放资源的场景。
  • 缺点

    • 每次访问都需要加锁,性能略低。
    • 需要关注引用计数的同步问题。

5. 对象销毁顺序与全局析构器

在多线程程序中,若单例需要在程序退出前手动销毁(例如释放文件句柄、网络连接等),可以使用 Meyers 单例 并结合 std::atexit 或自定义全局析构器:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    static void destroy() {
        // 如果使用静态局部变量,系统会自动销毁
        // 这里可以添加自定义清理逻辑
    }

private:
    Singleton() {}
};

int main() {
    Singleton::getInstance();   // 触发实例创建
    std::atexit(&Singleton::destroy);
    return 0;
}

6. 何时选择哪种实现?

场景 推荐实现 说明
C++11 及以后 Meyers 单例 最简洁、延迟加载、线程安全
需要自定义销毁顺序 Meyers + atexit 可控制析构时机
兼容旧标准 双重检查锁或 call_once 手动实现线程安全
需要在多次使用后释放 shared_ptr/weak_ptr 自动管理生命周期

7. 小结

  • 线程安全:C++11 之后 static 局部变量的初始化已保证原子性,推荐使用 Meyers 单例。
  • 延迟加载:所有实现默认在第一次访问时创建实例,避免不必要的资源占用。
  • 销毁顺序:若单例资源需手动释放,最好在 main 结束前调用 atexit 或使用 shared_ptr

通过上述实现方式,开发者可以根据项目需求、编译器版本以及资源管理策略,选取最合适的线程安全单例实现。

发表评论