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

在多线程环境下,单例模式需要确保只会被创建一次,并且所有线程都能安全地访问该实例。C++11 之后的标准提供了多种手段来实现线程安全的单例,下面从理论、实现和性能三方面进行详细剖析。

1. 理论基础

单例模式(Singleton)旨在保证某个类只有一个实例,并且提供全局访问点。传统的实现方式往往依赖于静态成员变量和懒初始化:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance; // 懒初始化
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

C++11 之前,该实现并不保证线程安全:多线程同时访问 getInstance() 可能导致多次构造。C++11 引入了 魔法静态初始化(magic statics),即在函数内部声明的局部静态变量在首次访问时会被线程安全地初始化,编译器会自动生成必要的互斥锁。

2. 三种主流实现方式

2.1 函数内局部静态(C++11 及以上)

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& instance() {
        static ThreadSafeSingleton obj; // 线程安全初始化
        return obj;
    }
private:
    ThreadSafeSingleton() = default;
    ~ThreadSafeSingleton() = default;
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
};

优点:代码简洁、无需显式锁。缺点:在第一次访问时需要动态链接库的线程同步机制,若单例初始化耗时较长,可能会导致程序启动时的性能瓶颈。

2.2 静态局部 + 显式互斥

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

std::unique_ptr <MutexSingleton> MutexSingleton::instancePtr;
std::once_flag MutexSingleton::initFlag;

使用 std::call_once 保证只执行一次初始化。优点:可在单例构造前做一些额外的初始化工作(如读取配置)。缺点:代码更复杂。

2.3 双重检查锁(DCL)+ 原子操作

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

std::atomic<DCLSingleton*> DCLSingleton::instancePtr{nullptr};
std::mutex DCLSingleton::mtx;

适用于旧标准(C++03)或需要极致性能的场景。需谨慎处理内存顺序,否则可能出现未定义行为。

3. 性能与资源管理

  • 懒初始化:只有在真正需要时才创建实例,避免不必要的资源占用。
  • RAII:单例的析构在程序结束时自动调用,避免手动 delete,防止内存泄漏。
  • 线程销毁:若单例内部持有线程(如日志线程),需在析构时优雅停止,否则可能导致程序崩溃。

4. 常见陷阱与误区

误区 说明 解决方案
认为 static 成员变量足够线程安全 早期编译器未实现线程安全的初始化 使用 C++11 之后的魔法静态或 std::call_once
单例构造抛异常导致程序崩溃 构造函数抛异常会破坏全局状态 在构造函数内部捕获异常或使用异常安全包装
认为全局单例是坏的 单例能减少传参,易于共享 只在必要时使用,避免过度依赖全局状态
忽略多进程环境 单例只在单进程内生效 对多进程共享需要使用进程间同步机制

5. 代码演示

下面给出一个完整示例:一个线程安全的日志单例,支持多线程写日志且不产生竞争。

#include <iostream>
#include <fstream>
#include <mutex>
#include <string>
#include <chrono>
#include <ctime>

class Logger {
public:
    static Logger& get() {
        static Logger instance; // C++11 线程安全初始化
        return instance;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(writeMutex);
        outFile << timestamp() << " " << msg << '\n';
    }

private:
    Logger() {
        outFile.open("log.txt", std::ios::out | std::ios::app);
        if (!outFile) {
            throw std::runtime_error("无法打开日志文件");
        }
    }
    ~Logger() {
        outFile.close();
    }

    std::string timestamp() {
        auto now = std::chrono::system_clock::now();
        std::time_t t = std::chrono::system_clock::to_time_t(now);
        char buf[20];
        std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
        return buf;
    }

    std::ofstream outFile;
    std::mutex writeMutex;

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

// 用法
void worker(int id) {
    for (int i = 0; i < 5; ++i) {
        Logger::get().log("线程 " + std::to_string(id) + " 写日志 " + std::to_string(i));
    }
}

该实现利用 C++11 的魔法静态保证 Logger 只会被构造一次,并通过 std::mutex 保护文件写入,避免多线程竞争。

6. 小结

  • C++11 起,函数内部局部静态已实现线程安全单例,是最简洁且安全的做法。
  • 对于特殊需求(如延迟配置、旧编译器),可采用 std::call_once 或 DCL + 原子。
  • 注意异常安全、资源释放和多进程环境。
  • 在实际项目中,应权衡单例带来的便利与潜在的全局状态风险。

通过上述方法,你可以在多线程 C++ 应用中安全、高效地实现单例模式。

发表评论