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

单例模式(Singleton Pattern)是一种常见的软件设计模式,用于确保一个类只有一个实例,并提供全局访问点。在多线程环境下实现线程安全的单例模式尤为重要,以避免竞争条件导致的错误实例化。下面我们从 C++11 开始,探讨几种常用且线程安全的实现方式。


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

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

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 优点:实现最简洁,使用局部静态变量,编译器保证初始化是线程安全的。无需手动加锁,消除了锁带来的性能开销。
  • 注意事项:在 C++11 之前的编译器中,局部静态变量的初始化并非线程安全。若在旧编译环境下,需要自行使用互斥锁。

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

#include <mutex>

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

    ~Singleton() { delete instance_; }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
  • 优点:避免了每次访问都加锁,只有在首次创建实例时才需要加锁。
  • 缺点:实现比较繁琐,易出错。由于编译器优化和 CPU 指令重排,早期的 C++ 实现需要使用 std::atomicstd::atomic_flag 来确保可见性。

3. std::call_oncestd::once_flag

#include <mutex>

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

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
  • 优点std::call_once 通过 std::once_flag 内部实现了一次性初始化的原子性。代码简洁,兼容 C++11 及以后版本。
  • 缺点:仍然使用裸指针,若想避免手动 delete,需要配合 std::unique_ptr

4. 用 std::unique_ptr 结合 std::call_once

#include <mutex>
#include <memory>

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag_, [](){
            instance_ = std::unique_ptr <Singleton>(new Singleton());
        });
        return *instance_;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    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_;
  • 优点:使用 unique_ptr 自动管理内存,避免泄漏。线程安全初始化与访问。

5. 对比与选择

方法 实现简洁度 线程安全性 性能 兼容性
Meyers 单例 ★★★★★ ★★★★★(C++11 以后) ★★★★★ C++11 以后
双重检查锁 ★★ ★★(取决实现) ★★★ 需要手动处理内存
call_once ★★★ ★★★★★ ★★★★ C++11 以后
call_once + unique_ptr ★★★★ ★★★★★ ★★★★ C++11 以后

在大多数现代 C++ 项目中,Meyers 单例std::call_once 是推荐的做法。它们代码量少、易于维护,并且性能接近。


6. 常见陷阱

  1. 延迟析构:如果你使用裸指针或 std::unique_ptr,确保在程序结束前析构单例,或者让它驻留在进程全生命周期内。
  2. 跨线程静态对象初始化:即使使用 call_once,也要注意多线程环境下的静态对象销毁顺序问题。
  3. 递归调用:如果在单例构造函数内部再次访问 instance(),会导致死循环或未定义行为。

7. 小结

  • 在 C++11 之后,使用局部静态变量(Meyers 单例)已足够满足大多数需求,且最简洁。
  • 对于需要手动控制生命周期或更细粒度的控制,std::call_oncestd::once_flag 提供了强大的工具。
  • 双重检查锁在现代 C++ 中已不太推荐,除非你处于非常老的编译环境。

掌握这些实现方式后,你可以根据项目需求选择最合适的单例实现,确保线程安全与高性能并存。祝编码愉快!

发表评论