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

在多线程环境下,单例模式的实现尤为重要,因为它必须保证在任何时刻只有一个实例存在,并且在并发情况下不会出现竞态条件。以下是一种常用且高效的实现方式,结合了 C++11 之后的特性:std::call_oncestd::once_flag

1. 基本思路

  • 懒汉式:实例在第一次使用时才创建,避免不必要的资源占用。
  • 线程安全:利用 std::call_once 确保单次初始化,即使多个线程同时请求也只会执行一次构造。
  • 懒加载:通过局部静态变量或 std::unique_ptr 延迟初始化。

2. 代码实现

#include <iostream>
#include <mutex>
#include <memory>

class Singleton {
public:
    // 提供全局访问点
    static Singleton& Instance() {
        // std::call_once 只会执行一次
        std::call_once(initFlag_, []() {
            instance_.reset(new Singleton);
        });
        return *instance_;
    }

    // 业务方法示例
    void doSomething() const {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

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

private:
    Singleton() {
        std::cout << "Singleton constructed at " << this << std::endl;
    }
    ~Singleton() = default;

    // 单例实例
    static std::unique_ptr <Singleton> instance_;
    // 用于控制一次初始化
    static std::once_flag initFlag_;
};

// 静态成员定义
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;

关键点说明

  1. std::call_oncestd::once_flag
    std::call_once 接受一个 std::once_flag 以及一个可调用对象(lambda、函数指针等)。无论有多少线程调用 Instance()initFlag_ 只会让其中一个线程执行 lambda,保证单例唯一性。

  2. 懒加载
    `std::unique_ptr

    ` 只在 `call_once` 里实例化,避免在程序启动时就创建。
  3. 线程安全的析构
    如果你需要在程序结束时销毁单例,使用 std::unique_ptr 可以自动在全局静态对象析构时销毁实例。若你想手动控制生命周期,可在 Singleton 内部实现 destroy() 方法,使用 std::call_once 确保只销毁一次。

  4. 禁止拷贝/赋值
    为了确保唯一性,删除拷贝构造和赋值运算符。

3. 使用示例

#include <thread>

void worker() {
    Singleton::Instance().doSomething();
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);
    std::thread t3(worker);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

运行结果将显示同一实例地址,证明只有一个实例被创建。

4. 进阶:C++17 以上的局部静态变量

C++11 之后,局部静态变量的初始化已是线程安全的。因此可以更简洁地实现:

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton instance;
        return instance;
    }
    // 其余同上
};

这种方式省略了 std::call_once,编译器保证线程安全。缺点是如果实例构造抛异常,后续访问会导致再次尝试初始化,可能会产生不确定行为。若构造函数不抛异常,推荐使用此方式。

5. 小结

  • 使用 std::call_oncestd::once_flag 可以在 C++11 以上安全实现线程安全单例。
  • 通过 std::unique_ptr 或局部静态变量完成懒加载与自动析构。
  • 删除拷贝/赋值以保持唯一性。
  • 对于性能敏感的场景,可考虑直接使用局部静态变量,因其在多数编译器中实现更高效。

这样,你就可以在多线程 C++ 程序中安全、简洁地使用单例模式了。

发表评论