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

在多线程环境下,单例(Singleton)模式需要保证实例的唯一性和线程安全。自C++11起,语言层面已经提供了对线程安全初始化的支持,简化了实现。下面从理论到代码演示几种常用的实现方式,并讨论它们的优缺点。

1. C++11 的“懒汉式”Meyers Singleton

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // 函数内静态对象
        return instance;
    }

    // 删除拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};

原理
C++11 规定,对函数内静态对象的初始化是 线程安全 的。编译器会在第一次调用 instance() 时执行一次初始化,后续调用不再重复。只要编译器遵守标准,这种方式既简单又高效。

优点

  • 代码最短,最易读。
  • 无需手动同步,避免死锁与竞态。
  • 延迟初始化(懒加载),在首次使用时才创建。

缺点

  • 需要 C++11 或更高。
  • 对析构时的销毁顺序(尤其是多线程结束时)有些微的不确定性。
  • 对于跨进程共享单例(如共享内存)无法直接使用。

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

#include <atomic>
#include <mutex>

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

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

private:
    Singleton() = default;
    ~Singleton() = default;

    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;

原理

  • 第一次检查 instance_ 是否已创建,若未创建则进入锁区。
  • 再次检查(再次锁定前)确认实例仍未创建,避免多线程同时创建多份实例。
  • 使用 std::atomic 与内存序保证可见性。

优点

  • 兼容 C++11 之前的编译器(但需要显式同步)。
  • 控制细粒度的锁,性能相对良好。

缺点

  • 代码较繁琐,易出错。
  • 需要手动管理 delete,若程序退出时未释放会造成内存泄漏。
  • 需要 std::atomic 的正确使用,错误的内存序会导致数据竞争。

3. 静态局部变量与 std::call_once

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

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

private:
    Singleton() = default;
    ~Singleton() = default;

    static Singleton* instance_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;

原理

  • std::call_once 确保给定的 lambda 只执行一次。
  • std::once_flag 负责同步,内部实现使用原子操作和锁。

优点

  • 与双重检查锁定相似,但代码更简洁。
  • 支持 C++11 及以上。

缺点

  • std::call_once 的实现细节相关,某些老旧编译器可能不完美。
  • 与静态局部变量相比,缺少自动销毁(除非在 atexit 注册销毁函数)。

4. 适配多进程环境的单例(共享内存单例)

在多进程共享内存时,单例需要在共享内存段内创建。下面演示一种基于 boost::interprocess 的实现思路(可根据需求替换为 POSIX shm_open/mmap 等):

#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>

struct Singleton {
    Singleton() = default;
    // 业务成员...
};

Singleton* getSingleton() {
    using namespace boost::interprocess;
    static managed_shared_memory segment(open_or_create, "MySharedMemory", 65536);
    static interprocess_mutex mutex;
    static Singleton* instance = nullptr;

    if (!instance) {
        boost::interprocess::scoped_lock <interprocess_mutex> lock(mutex);
        if (!instance) {
            instance = segment.construct <Singleton>("SingletonInstance")();
        }
    }
    return instance;
}

说明

  • managed_shared_memory 在共享内存区创建对象。
  • interprocess_mutex 处理进程间同步。
  • 适用于多进程共享同一实例(如数据库连接池、缓存等)。

5. 何时使用哪种实现?

场景 推荐实现 备注
单进程、C++11+ Meyers Singleton 最简洁,线程安全
旧编译器(C++03) 双重检查锁定 需要手动同步
需要手动销毁 std::call_once + 自定义析构 兼顾性能与可控性
多进程共享 共享内存 + 进程间同步 复杂度更高,需考虑映射、权限

6. 常见陷阱与最佳实践

  1. 删除拷贝构造/赋值:避免被复制产生多个实例。
  2. 懒加载 vs 预初始化:若实例化成本高且启动阶段不需要,使用懒加载;若想避免启动时的延迟,考虑在程序初始化阶段显式创建。
  3. 销毁顺序:静态局部对象在程序退出时按逆序销毁;若涉及跨文件的静态单例,需小心析构顺序。
  4. 异常安全:若构造函数抛异常,静态局部对象会再次尝试初始化,确保异常不导致程序崩溃。
  5. 多线程测试:在多核机器上使用 std::thread 并发访问 instance(),验证线程安全。

7. 小结

  • C++11 为单例提供了最直接的线程安全实现:静态局部对象。
  • 对于更旧的环境或更细粒度的控制,可使用双重检查锁定或 std::call_once
  • 多进程共享单例需要进程间同步与共享内存。
  • 关键在于删除拷贝避免析构顺序问题保证线程安全

通过以上方式,你可以根据项目需求和编译环境,选用最合适的单例实现,既保证了线程安全,又保持了代码的可维护性。

发表评论