**标题:**

如何在现代 C++(C++17/20)中实现线程安全的单例模式?

正文:

在多线程环境下,单例模式常用于共享资源(如日志器、配置管理器、数据库连接池等)。传统的单例实现容易产生竞争条件或双重检查锁定(Double-Check Locking)缺陷。自 C++11 起,标准库提供了对线程安全的静态局部变量初始化的保证,结合 std::call_once,我们可以轻松实现高效且安全的单例。

下面给出一个完整的实现示例,并说明其工作原理、性能特点以及常见误区。


1. 需求分析

  • 单例对象:只能有一个实例。
  • 懒加载:对象在第一次使用时才创建。
  • 线程安全:多线程并发访问时不会产生竞态。
  • 高性能:创建后每次访问不需要加锁。

2. 关键技术点

  1. 局部静态变量

    • C++11 之后,局部静态变量的初始化是线程安全的。第一次进入作用域时,编译器会生成必要的同步代码。
    • 适合懒加载,避免一次性构造全局对象导致的“静态初始化顺序问题”。
  2. std::call_oncestd::once_flag

    • 通过 std::call_once 可以在多线程环境中保证某个函数只被调用一次,常用于实现单例或延迟初始化。
    • std::once_flag 配合使用。
  3. 构造函数私有化

    • 防止外部直接实例化,保持单例完整性。
  4. 删除拷贝与移动构造

    • 防止复制或移动导致多个实例。

3. 代码实现

// singleton.hpp
#pragma once
#include <mutex>
#include <memory>
#include <iostream>

// 线程安全的单例模板(C++17 兼容)
template <typename T>
class Singleton
{
public:
    // 获取单例实例(引用)
    static T& instance()
    {
        // 1. 静态局部变量,保证懒加载且线程安全
        static std::once_flag init_flag;
        static std::unique_ptr <T> ptr;

        // 2. 仅初始化一次
        std::call_once(init_flag, []{
            ptr = std::make_unique <T>();
        });

        return *ptr;
    }

    // 禁止拷贝构造和移动构造
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

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

// 业务类示例:日志器
class Logger : private Singleton <Logger>
{
    friend class Singleton <Logger>;  // 允许 Singleton 访问构造函数

public:
    void log(const std::string& msg)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[LOG] " << msg << std::endl;
    }

private:
    Logger() { std::cout << "Logger constructed\n"; }
    std::mutex mutex_;
};

使用方式

#include "singleton.hpp"
#include <thread>

void worker(int id)
{
    auto& logger = Logger::instance();   // 线程安全获取
    logger.log("Worker " + std::to_string(id) + " started");
}

int main()
{
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);

    t1.join(); t2.join(); t3.join();
    return 0;
}

运行结果(示例)

Logger constructed
[LOG] Worker 1 started
[LOG] Worker 2 started
[LOG] Worker 3 started

4. 细节说明

  1. 构造顺序

    • 由于使用 std::once_flagstd::call_onceptr 的初始化在第一次调用 instance() 时完成,避免了静态初始化顺序错误(static initialization order fiasco)。
  2. 异常安全

    • 如果 T 的构造函数抛异常,std::call_once 会再次尝试调用,直至成功为止。
  3. 销毁时机

    • `unique_ptr ` 的析构会在程序结束时自动销毁单例实例。若需自定义销毁时机,可将 `ptr` 替换为 `shared_ptr` 或手动管理。
  4. 性能

    • 第一次调用需要 std::call_once 的同步;随后调用只需访问静态局部变量,无需加锁,几乎无开销。
  5. 多继承

    • 如果业务类多继承自多个单例,可能导致二义性。可使用 CRTP(Curiously Recurring Template Pattern)或 std::shared_ptr 的组合方式解决。

5. 常见误区

误区 说明
使用宏实现单例 宏无法捕获异常,缺乏类型安全,难以维护。
单例作为全局对象 可能导致全局初始化顺序问题。
手动加锁 过度加锁会导致性能下降;正确使用 std::call_once 可避免。
未删除拷贝构造 允许复制会破坏单例。
在 C++11 之前使用局部静态 不是线程安全,需使用 std::call_once 或其他同步。

6. 进一步阅读

  • Herb Sutter,“C++ Concurrency in Action”(第 3 章:单例与懒加载)
  • Bjarne Stroustrup,“The C++ Programming Language”(第 13 章:单例模式)
  • ISO C++ 标准草案([N4861])关于 “statics” 的线程安全保证

结语

通过结合 C++11 的线程安全局部静态初始化与 std::call_once,我们可以在不牺牲性能的前提下,实现高效且安全的单例。只需遵循上述模板,即可在任何业务类中快速部署单例模式,减少手工同步的麻烦,让代码更简洁、可靠。

发表评论