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

在多线程环境下,单例模式需要确保只有一个实例被创建,并且在并发访问时不会出现竞争条件。C++17 以后可以利用 std::call_oncestd::once_flag 来实现最简单、最安全的单例。下面从设计思路、实现细节和性能考量三个角度,逐步阐述如何编写一个线程安全的单例。


1. 设计思路

  1. 懒汉式(Lazy)
    • 只在第一次使用时创建实例,避免不必要的初始化成本。
  2. 线程安全
    • 使用 std::call_once 确保初始化只执行一次。
  3. 避免“双重检查锁定”
    • 双重检查锁定(Double-Check Locking)在某些编译器/硬件上仍可能产生数据竞争。
  4. 保证对象在整个程序生命周期内有效
    • 单例实例应在程序结束前保持存在,或使用 std::shared_ptr 与自定义删除器管理生命周期。

2. 实现代码

#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>

class ThreadSafeSingleton {
public:
    // 提供全局访问点
    static ThreadSafeSingleton& Instance() {
        std::call_once(initFlag_, &ThreadSafeSingleton::Init);
        return *instance_;
    }

    // 禁止复制与移动
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton(ThreadSafeSingleton&&) = delete;
    ThreadSafeSingleton& operator=(ThreadSafeSingleton&&) = delete;

    // 示例业务函数
    void DoWork() {
        std::cout << "Thread " << std::this_thread::get_id() << " is using singleton at address " << this << "\n";
    }

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

    static void Init() {
        instance_ = std::unique_ptr <ThreadSafeSingleton>(new ThreadSafeSingleton);
    }

    static std::once_flag initFlag_;
    static std::unique_ptr <ThreadSafeSingleton> instance_;
};

// 静态成员定义
std::once_flag ThreadSafeSingleton::initFlag_;
std::unique_ptr <ThreadSafeSingleton> ThreadSafeSingleton::instance_ = nullptr;

// 简单测试
int main() {
    constexpr int threadCount = 10;
    std::vector<std::thread> workers;

    for (int i = 0; i < threadCount; ++i) {
        workers.emplace_back([]{
            ThreadSafeSingleton::Instance().DoWork();
        });
    }

    for (auto& t : workers) t.join();
    return 0;
}

说明:

  • std::call_oncestd::once_flag 保证 Init() 只会被调用一次,所有线程在第一次访问 Instance() 时会阻塞直到实例完成初始化。
  • 由于使用 unique_ptr,单例在程序退出时会被正确析构。若需要更细粒度的控制(例如懒销毁),可以改用 shared_ptr 或手动管理析构。
  • Instance() 返回引用,调用者无需担心内存泄漏。

3. 性能与可读性评估

方案 初始化开销 访问开销 可读性 线程安全 适用场景
static local(C++11) 低(只需一次) 低(无锁) 通过编译器实现 轻量级
std::call_once 低(只一次) 低(无锁) 需要显式控制
double-checked locking 低(锁粒度小) 需细心 避免 call_once 的实现细节
  • static local:最简洁,C++11 标准保证线程安全。
  • call_once:更直观地表明“只调用一次”,适合需要手动管理生命周期或需要在特定时机初始化的场景。

4. 常见错误与陷阱

错误 说明 解决方案
在构造函数里调用单例 可能导致递归调用 避免在构造函数里访问 Instance()
使用裸指针 可能导致悬挂指针 使用 unique_ptrshared_ptr
忘记删除拷贝构造/赋值 可能导致多个实例 delete 拷贝构造/赋值运算符
多次包含头文件导致重复定义 链接错误 使用 include guards 或 #pragma once

5. 小结

  • std::call_once 是 C++ 中最推荐的线程安全单例实现方式。
  • 对于大多数场景,static local(C++11 及以后)足够简洁且安全;如果需要更细粒度的控制或在类外初始化,则 call_once 是更好的选择。
  • 记住禁用拷贝/移动构造和赋值运算符,确保单例的唯一性。

通过上述实现,你可以在任何多线程 C++ 应用中安全、可靠地使用单例模式,而不必担心竞争条件或初始化问题。

发表评论