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

在多线程环境中,单例模式常常需要保证在所有线程中只创建一次实例,并且保证线程安全。下面介绍几种实现方式,并说明它们的优缺点。

1. 使用 C++11 的 std::call_once

#include <iostream>
#include <mutex>

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

    void doSomething() { std::cout << "Doing something\n"; }

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::unique_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

int main() {
    auto& s1 = Singleton::getInstance();
    auto& s2 = Singleton::getInstance();
    s1.doSomething();
    return 0;
}

优点

  • 线程安全std::call_once 只在第一次调用时执行一次初始化。
  • 延迟初始化:只有第一次调用 getInstance() 时才会创建实例。
  • 简洁:不需要手动加锁,避免死锁和性能损耗。

缺点

  • 需要 C++11 及以上标准支持。

2. 饿汉式(Eager Initialization)

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

Singleton Singleton::instance{};
  • 线程安全性:在 C++11 之后,静态对象的初始化是线程安全的。
  • 缺点:实例在程序启动时就会创建,可能会导致资源浪费或死锁问题。

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

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
private:
    Singleton() = default;
    static Singleton* instance;
    static std::mutex mtx;
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
  • 优点:延迟初始化,首次进入时不需要锁,后续访问更快。
  • 缺点:在 C++11 之前,可能出现指令重排导致的线程安全问题。即使在 C++11 之后,也需要使用 std::atomicstd::mutex 来保证可见性。

4. Meyers 单例(函数内部静态对象)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() = default;
};
  • 简洁且安全:C++11 保证了局部静态变量初始化的线程安全。
  • 延迟:只有第一次调用 getInstance() 时才会初始化。

5. 通过 std::shared_ptrstd::call_once

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::call_once(initFlag, [](){
            instance = std::make_shared <Singleton>();
        });
        return instance;
    }
private:
    Singleton() = default;
    static std::shared_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::shared_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
  • 优点:返回智能指针,自动管理生命周期,避免手动 delete。
  • 缺点:需要注意循环引用问题。

小结

  • 推荐使用:C++11 及以上时,std::call_once 或 Meyers 单例是最简洁、安全的实现方式。
  • 性能:Meyers 单例在大多数实现中已足够高效,只有极端高频访问时才考虑微调。
  • 跨平台:上述实现都依赖标准库,具有良好的可移植性。

在实际项目中,最重要的是保持代码简洁、可读,并且在多线程环境下确保正确性。根据项目的具体需求(如是否需要手动销毁、资源占用是否可接受等)选择合适的实现方式。

发表评论