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

在多线程环境下实现一个线程安全的单例模式,最常见的做法是使用 C++11 引入的局部静态变量初始化以及 std::call_once。下面分别介绍这两种方法,并给出完整可编译的代码示例。

1. C++11 的局部静态变量初始化

C++11 标准保证了对局部静态对象的初始化是线程安全的。只需要把单例的实例放在一个函数内部的静态局部变量即可。

#include <iostream>
#include <mutex>

class Singleton {
public:
    // 获取单例实例
    static Singleton& instance() {
        static Singleton instance;   // C++11 保证线程安全
        return instance;
    }

    // 禁止复制和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    void doSomething() { std::cout << "Doing something in thread " << std::this_thread::get_id() << '\n'; }

private:
    Singleton() { std::cout << "Singleton constructed\n"; }
    ~Singleton() { std::cout << "Singleton destructed\n"; }
};

int main() {
    // 创建若干线程同时访问单例
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([]{
            Singleton::instance().doSomething();
        });
    }
    for (auto& t : threads) t.join();
    return 0;
}

关键点

  • static Singleton instance; 在第一次调用 instance() 时初始化,后续调用直接返回同一实例。
  • 标准保证了初始化过程中的“单一写入”与“可见性”,因此不需要显式的互斥锁。
  • 通过删除拷贝构造、赋值操作等,防止了单例被复制。

2. 使用 std::call_once

如果你想要更细粒度地控制初始化过程,或者在旧编译器(C++11 之前)上工作,可以使用 std::call_once

#include <iostream>
#include <mutex>
#include <thread>

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

    void doSomething() {
        std::cout << "Doing something in thread " << std::this_thread::get_id() << '\n';
    }

private:
    Singleton() { std::cout << "Singleton constructed\n"; }
    ~Singleton() { std::cout << "Singleton destructed\n"; }

    static Singleton* instancePtr;
    static std::once_flag initFlag;
};

// 需要在 cpp 文件中定义静态成员
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([]{
            Singleton::instance().doSomething();
        });
    }
    for (auto& t : threads) t.join();
    return 0;
}

关键点

  • std::call_once 确保 lambda 只被执行一次,即使多个线程同时调用。
  • std::once_flag 必须是 static,且与 call_once 配合使用。
  • 需要手动管理单例指针的生命周期,或者使用 std::unique_ptr 自动析构。

3. 对比与选择

方法 适用编译器 代码简洁度 线程安全实现方式
局部静态变量 C++11 及以上 最简洁 编译器内部实现
std::call_once C++11 及以上 稍复杂 通过 once_flag 与 call_once 保障
  • 若项目已使用 C++11 或更高版本,优先选择局部静态变量,因为代码更短且不需要额外的同步对象。
  • 若想在旧编译器上兼容,或者需要在单例初始化时执行更复杂的逻辑std::call_once 更灵活。

4. 进一步考虑

  1. 懒加载 vs 预初始化:局部静态变量是在第一次访问时才初始化,满足懒加载需求。若需要在程序启动时就创建单例,可在 main 入口处显式调用一次 Singleton::instance()
  2. 销毁顺序:使用局部静态变量时,实例会在程序退出时按逆序析构;使用 std::call_once 创建的裸指针需要自行释放,建议使用 std::unique_ptrstd::shared_ptr
  3. 异常安全:如果单例构造函数抛异常,std::call_once 需要重新执行;局部静态变量在抛异常后会再次尝试初始化,符合规范。

结语

线程安全的单例模式是多线程 C++ 开发中的常见需求。C++11 的特性大大简化了实现:局部静态变量初始化即可保证线程安全;若需要更灵活的初始化策略,std::call_once 是可靠的选择。根据项目的编译器版本和具体需求,选择合适的方法即可实现安全、简洁的单例。

发表评论