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

在 C++11 之后,标准库提供了多种实现线程安全单例模式的手段。本文将从语言特性、常见实现方式以及实际应用场景几个角度,系统阐述如何在现代 C++ 中安全地实现单例。

1. 单例模式的基本思路

单例模式要求在整个程序生命周期内,某个类只能有唯一的实例。传统实现往往使用私有构造函数、静态成员指针以及公开的 getInstance() 接口来完成。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 1. 静态局部对象
        return instance;
    }
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

2. 线程安全的关键点

在多线程环境下,最常见的竞态条件是:两条线程同时进入 getInstance(),导致两个不同的 Singleton 实例被创建。为避免此类情况,需要确保实例化过程是原子且可重入的。

2.1 C++11 的静态局部变量初始化

自 C++11 起,局部静态变量的初始化是线程安全的。这意味着上面代码中的 static Singleton instance; 在第一次被访问时会自动被保护,避免了多线程重复初始化。无论多少线程同时调用 getInstance(),编译器会插入必要的锁机制。

2.2 std::call_oncestd::once_flag

如果你想手动控制初始化,或者需要在构造过程中执行复杂逻辑(例如读取配置文件、连接数据库等),可以使用 std::call_once

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, [](){
            instance.reset(new Singleton());
        });
        return *instance;
    }
private:
    Singleton() = default;
    static std::unique_ptr <Singleton> instance;
    static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;

std::call_once 保证给定 lambda 只会执行一次,即使多线程并发调用也能保持安全。

3. 延迟销毁与 std::shared_ptr

在 C++11 之前,单例往往采用 delete 在程序退出时手动销毁。然而,在多线程环境中,析构顺序问题可能导致未定义行为。使用 std::shared_ptr 并结合 std::weak_ptr 可以让单例对象在最后一次引用失效时自动销毁:

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;
std::once_flag Singleton::initFlag;

这样,即使多个线程持有 std::shared_ptr,对象也会在最后一次引用消失时安全析构。

4. 在类内部实现单例(友元技术)

有时你希望单例只在类内部使用,外部无法获取引用。可以将 getInstance() 设为私有,并使用友元类或内部结构访问:

class Singleton {
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    class Accessor {
    public:
        static Singleton& get() {
            static Singleton instance;
            return instance;
        }
    };
};

此时,只有 Accessor 能够访问单例实例,外部无法直接调用。

5. 性能与可见性考虑

  • 局部静态变量:首次访问时会有一次锁竞争,之后访问速度与普通局部变量无异。
  • std::call_once:同样会有一次锁竞争,适用于一次性初始化。若初始化非常昂贵,使用此法可以减少不必要的同步。
  • std::atomic:若你仅需在多线程间保证可见性(不需要同步初始化),可以使用 std::atomic<Singleton*> 来实现双检锁(double‑checked locking)。但要注意内存模型和可见性,避免出现指针先写后读的情况。

6. 实际案例:日志系统单例

class Logger {
public:
    static Logger& instance() {
        static Logger inst;  // 线程安全
        return inst;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mtx);
        std::cout << "[" << std::chrono::system_clock::now().time_since_epoch().count() << "] " << msg << '\n';
    }

private:
    Logger() = default;
    std::mutex mtx;
};

在多线程环境下,每个线程都可以通过 Logger::instance() 写日志,内部的 mtx 保证输出顺序一致。

7. 总结

  • C++11 为单例提供了天然的线程安全机制:局部静态变量和 std::call_once
  • 选择哪种实现方式取决于初始化成本、销毁需求以及是否需要手动控制初始化。
  • 在实际项目中,建议使用局部静态变量或 std::call_once,避免手动实现锁机制以减少错误。
  • 对于需要延迟销毁的场景,可考虑 std::shared_ptr

掌握这些技术后,你可以在任何需要全局唯一对象的地方,安全、简洁地实现单例模式。

发表评论