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

在 C++ 中实现单例模式时,最常见的挑战之一就是保证在多线程环境下的线程安全。下面将介绍几种常用的实现方式,并比较它们的优缺点。


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

class Singleton {
public:
    static Singleton& getInstance() {
        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)

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; }

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

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
  • 优点:显式控制实例创建时机,支持延迟初始化。
  • 缺点:实现错误多(尤其是 volatile 的使用),在 C++11 之前编译器的优化可能导致实例创建不安全。

3. std::call_oncestd::once_flag

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

private:
    Singleton() = default;
    static std::unique_ptr <Singleton> instance;
    static std::once_flag flag;
};

std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::flag;
  • 优点:标准化、跨平台,保证线程安全,延迟初始化。
  • 缺点:需要 C++11,若对销毁时机有严格要求,可能需要手动销毁。

4. 模板化单例(多类型单例)

template<typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};

使用示例:

struct Config { /* ... */ };
Config& cfg = Singleton <Config>::getInstance();
  • 优点:可为不同类型提供独立的单例实例。
  • 缺点:与 Meyers 单例相同,销毁顺序不确定。

5. 资源泄漏与防止手动删除

单例通常在程序整个生命周期内存在,手动删除实例可能会导致程序在退出时产生错误。可以使用 std::shared_ptr 并让其在程序结束时自动释放,或者使用 std::atexit 注册销毁函数。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        std::atexit(&Singleton::destroy);
        return instance;
    }
private:
    Singleton() = default;
    static void destroy() {
        // 进行必要的清理工作
    }
};

6. 性能比较

方法 延迟初始化 线程安全保证 开销
Meyers ✅(C++11) 极低
双重检查 ✅(需细心) 中等
call_once 中等
模板化 ✅(C++11) 极低

在实际项目中,推荐使用 Meyers 单例std::call_once 作为首选,因为它们既简洁又可靠。只有在需要手动控制销毁顺序或多类型单例时,才考虑使用模板化或双重检查锁。


7. 小结

  • Meyers 单例:最简单,C++11 之后线程安全,适合大多数场景。
  • std::call_once:标准化、延迟初始化,适用于需要更细粒度控制时。
  • 双重检查锁:在旧代码基中可能见到,但实现细节复杂,易出错。
  • 模板化单例:满足多类型单例需求,但销毁顺序仍需注意。

通过掌握这些实现方式,开发者可以在 C++ 程序中安全、高效地使用单例模式。

发表评论