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

在多线程环境下,单例模式需要保证只有一个实例被创建,并且在所有线程之间共享同一个实例。下面我们从几种常见实现方式入手,详细阐述它们的工作原理、优缺点以及最佳实践。

1. 饿汉式(Eager Initialization)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // 在第一次调用时创建
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 线程安全static 局部变量在 C++11 之后的实现是线程安全的。编译器会在第一次访问 instance() 时使用内部锁保证单例初始化仅执行一次。
  • 代码简洁:不需要手动管理锁或使用 std::call_once

缺点

  • 饿汉式:如果实例创建开销大且程序可能不使用单例,仍会在程序启动时就实例化,造成资源浪费。
  • 缺乏延迟:无法控制实例何时创建。

2. 懒汉式 + std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []() { instance_ = new Singleton(); });
        return *instance_;
    }
    ~Singleton() { delete instance_; }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static std::once_flag flag_;
    static Singleton* instance_;
};

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

优点

  • 延迟初始化:只有真正调用 instance() 时才创建对象。
  • 线程安全std::call_once 在多线程环境下只会执行一次回调,保证单例唯一。

缺点

  • 手动内存管理:需要显式删除对象,否则会导致内存泄漏(在 main() 结束前删除或使用 std::unique_ptr)。
  • 略显繁琐:需要维护 once_flag 与指针。

3. 双重检查锁(Double-Check Locking)— 不推荐

class Singleton {
public:
    static Singleton* instance() {
        if (!instance_) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) {
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
private:
    Singleton() {}
    static std::mutex mutex_;
    static Singleton* instance_;
};

说明:在 C++11 之前,编译器对内存模型的支持不完善,导致双重检查锁可能出现可见性问题。C++11 之后已经可以安全实现,但仍不如 std::call_once 简洁。

4. 使用 C++17 的 inline static

class Singleton {
public:
    static Singleton& instance() {
        static inline Singleton instance;
        return instance;
    }
private:
    Singleton() {}
};

inline static 让成员变量可以在头文件中定义,避免多重定义错误。此实现与饿汉式相同,但更现代。

5. 关键点总结

方法 线程安全 延迟 代码复杂度 适用场景
饿汉式 简单 资源小,应用必需
std::call_once 中等 需要延迟且资源较大
双重检查锁 旧代码兼容,慎用
inline static 简单 C++17 及以上

6. 实际案例:线程池单例

class ThreadPool {
public:
    static ThreadPool& getInstance(std::size_t threads = std::thread::hardware_concurrency()) {
        std::call_once(flag_, [threads](){ instance_ = new ThreadPool(threads); });
        return *instance_;
    }
    void submit(std::function<void()> task) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex_);
            tasks_.emplace(std::move(task));
        }
        cond_.notify_one();
    }
private:
    ThreadPool(std::size_t threads) : stop_(false) {
        for (std::size_t i = 0; i < threads; ++i)
            workers_.emplace_back([this](){ this->worker(); });
    }
    void worker() {
        while (true) {
            std::function<void()> task;
            {
                std::unique_lock<std::mutex> lock(queue_mutex_);
                cond_.wait(lock, [this](){ return stop_ || !tasks_.empty(); });
                if (stop_ && tasks_.empty()) return;
                task = std::move(tasks_.front());
                tasks_.pop();
            }
            task();
        }
    }
    ~ThreadPool() { stop(); }
    void stop() {
        {
            std::lock_guard<std::mutex> lock(queue_mutex_);
            stop_ = true;
        }
        cond_.notify_all();
        for (auto& w : workers_) w.join();
    }
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex queue_mutex_;
    std::condition_variable cond_;
    bool stop_;
    static std::once_flag flag_;
    static ThreadPool* instance_;
};
std::once_flag ThreadPool::flag_;
ThreadPool* ThreadPool::instance_ = nullptr;

此实现演示了 std::call_once 与单例结合的完整线程池示例,体现了延迟初始化与多线程安全的实际应用。

7. 小结

  • C++11 以后,std::call_once局部静态变量是实现线程安全单例最推荐的方式。
  • 饿汉式最为简洁,但缺乏延迟;懒汉式结合 call_once 兼顾延迟与安全。
  • 双重检查锁在现代 C++ 中不再必要,除非你必须兼容旧标准。
  • 对于 C++17,inline static 让单例实现更简洁。

遵循这些原则,你可以在任何 C++ 项目中安全、可靠地使用单例模式。

发表评论