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

在多线程环境下,单例模式(Singleton)需要保证即使多个线程同时访问,也只能创建一次实例。C++11之后提供了原子操作、内存序以及线程安全的静态局部变量,利用这些特性可以非常简洁地实现线程安全的单例。

1. 基础思路

单例的核心要求是:

  1. 私有化构造函数,防止外部直接实例化;
  2. 提供全局访问接口,返回唯一实例;
  3. 保证线程安全,在并发环境下只创建一次实例。

2. 使用C++11的静态局部变量

C++11 标准规定,函数内的静态局部变量在第一次使用时是线程安全初始化的。基于此,最简洁的实现如下:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // 线程安全的局部静态变量
        return instance;
    }

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

    // 业务方法
    void do_something() {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton constructed\n";
    }
};

调用方式:

int main() {
    auto& s1 = Singleton::instance();
    auto& s2 = Singleton::instance();
    s1.do_something();
    s2.do_something();
    return 0;
}
  • 第一次调用 instance() 时,Singleton 的构造函数被执行;
  • 之后的调用直接返回同一对象。

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

在某些旧版本或不支持C++11的编译器下,常见的实现是双重检查锁:

class Singleton {
public:
    static Singleton* instance() {
        if (inst_ == nullptr) {                     // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (inst_ == nullptr) {                 // 第二次检查
                inst_ = new Singleton();
            }
        }
        return inst_;
    }

    // 禁止拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
    static Singleton* inst_;
    static std::mutex mutex_;
};

// 定义静态成员
Singleton* Singleton::inst_ = nullptr;
std::mutex Singleton::mutex_;

然而,双重检查锁在没有适当的内存序保证时可能出现指令重排导致的可见性问题。C++11 提供了 std::atomic,可以更安全地实现:

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = inst_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = inst_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                inst_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
private:
    Singleton() {}
    static std::atomic<Singleton*> inst_;
    static std::mutex mutex_;
};

4. 智能指针和销毁

如果你需要在程序结束时自动销毁单例,可以使用 std::unique_ptr 与自定义删除器:

class Singleton {
public:
    static Singleton& instance() {
        static std::unique_ptr <Singleton> ptr{new Singleton};
        return *ptr;
    }
private:
    Singleton() {}
};

此实现与静态局部变量等价,但更符合现代C++的资源管理理念。

5. 性能对比

  • 静态局部变量:最优实现,编译器保证线程安全,运行时开销几乎为0。
  • 双重检查锁:需要加锁和原子操作,适用于需要延迟初始化且可能存在多次调用的场景。
  • 智能指针实现:语义清晰,兼容多种资源管理需求,但额外的 unique_ptr 可能带来轻微开销。

6. 典型使用场景

  1. 全局配置管理:读取配置文件后存放在单例中,供全局访问。
  2. 日志系统:单例日志对象保证所有模块写入同一日志。
  3. 数据库连接池:统一管理连接,避免多余连接。

7. 小结

  • C++11 之后,最推荐的做法是利用线程安全的静态局部变量;
  • 旧编译器可使用双重检查锁配合 std::atomic
  • 通过 unique_ptr 可以实现更细粒度的销毁控制。

掌握上述技术后,你就能在任何多线程 C++ 项目中安全、简洁地使用单例模式。

发表评论