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

在多线程环境下实现单例模式,最关键的是保证实例在任何线程里都只被创建一次,同时不产生性能瓶颈。下面给出几种常用的实现方式,并讨论它们的优缺点。


1. C++11 后的静态局部变量

class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj; // C++11 之后的初始化是线程安全的
        return obj;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  • 优点

    • 代码简洁,几乎没有额外开销。
    • 编译器保证线程安全(C++11 标准中对函数内部静态局部变量的初始化是原子且只执行一次)。
    • 延迟初始化(仅在第一次调用时创建)。
  • 缺点

    • 需要 C++11 或更高版本。
    • 如果实例需要在全局析构时做特殊清理,可能会出现“static deinitialization order fiasco”。

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

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instance_;
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_)
                instance_ = new Singleton();
        }
        return instance_;
    }
    static void destroy() {
        std::lock_guard<std::mutex> lock(mutex_);
        delete instance_;
        instance_ = nullptr;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

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

    • 可以在 C++11 之前使用(需手工实现同步)。
    • 只在第一次创建时加锁,后续访问几乎不受锁的影响。
  • 缺点

    • 需要正确使用 std::atomic,否则可能出现内存可见性问题。
    • 代码复杂,易出错。

3. std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){ ptr_ = new Singleton(); });
        return *ptr_;
    }
    static void destroy() {
        delete ptr_;
        ptr_ = nullptr;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    static Singleton* ptr_;
    static std::once_flag flag_;
};

Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
  • 优点

    • 标准库直接提供线程安全的一次性初始化。
    • 代码比双重检查锁更简洁、更安全。
    • 适用于 C++11 及以上。
  • 缺点

    • 仍需要手动销毁(如果需要)。
    • 对于非常频繁的访问,call_once 的内部实现会做一次状态检查,开销略高于静态局部变量。

4. 对象池方式(适用于需要多次创建和销毁的场景)

如果单例只在某些时段才需要存在,而不是一直占用内存,可以使用对象池或智能指针:

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!instance_ || instance_.expired()) {
            instance_ = std::make_shared <Singleton>();
        }
        return instance_.lock();
    }
private:
    Singleton() = default;
    static std::weak_ptr <Singleton> instance_;
    static std::mutex mutex_;
};

std::weak_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;
  • 优点

    • 允许多次销毁和重新创建,适合资源需要按需释放的情况。
    • 使用 shared_ptr 方便管理生命周期。
  • 缺点

    • 需要额外的锁保护。
    • 对于单例的传统定义(永不销毁)可能不合适。

选择建议

场景 推荐实现
需要最简洁、性能最优且使用 C++11+ 静态局部变量
需要手动销毁实例 std::call_once + 手动销毁
必须兼容 C++11 之前 双重检查锁(注意 std::atomic
需要按需销毁和重建 对象池 + weak_ptr

结语
在 C++ 中实现线程安全的单例并不需要复杂的设计,现代标准已经提供了非常方便且安全的工具。选择合适的实现方式,既能保持代码的简洁性,又能满足特定的性能或生命周期需求。

发表评论