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

在 C++11 之后,标准库提供了原子操作、互斥量以及线程等多种并发工具,使得在多线程环境下实现一个线程安全的单例模式变得既简洁又高效。下面我们将以一个“日志管理器”类为例,演示如何使用 std::call_oncestd::once_flag 来实现懒汉式单例,并讨论它的优缺点、常见误区以及可扩展的改进方案。


1. 单例模式简介

单例模式(Singleton Pattern)是一种创建型设计模式,用来确保一个类只有一个实例,并提供全局访问点。传统的懒汉式单例在多线程环境下存在竞争条件,容易导致多实例产生。为了解决这个问题,C++11 引入了线程安全的初始化机制,开发者可以直接利用它来实现单例。


2. 代码实现

#include <iostream>
#include <mutex>
#include <string>
#include <chrono>
#include <thread>

class Logger
{
public:
    // 通过静态成员函数获取单例对象
    static Logger& Instance()
    {
        std::call_once(initFlag_, []() {
            instance_ = new Logger();
        });
        return *instance_;
    }

    // 记录日志
    void Log(const std::string& msg)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[" << ++counter_ << "] " << msg << std::endl;
    }

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

private:
    Logger() = default;  // 私有构造
    ~Logger() = default; // 私有析构

    static Logger* instance_;
    static std::once_flag initFlag_;
    std::mutex mutex_;
    int counter_ = 0;
};

// 静态成员初始化
Logger* Logger::instance_ = nullptr;
std::once_flag Logger::initFlag_;

关键点说明

  1. std::call_once + std::once_flag

    • std::call_once 确保其内部的 lambda 函数仅被调用一次,即使有多个线程同时进入 Instance()
    • initFlag_ 负责跟踪初始化状态,内部使用锁实现原子性。
  2. 懒汉式初始化

    • instance_ 仅在第一次访问 Instance() 时被创建,延迟实例化。
  3. 线程安全的成员访问

    • Log 方法内部使用 std::lock_guard<std::mutex> 来保证多线程写日志时不出现竞争。
  4. 删除拷贝构造和赋值

    • 防止外部错误拷贝导致多实例。

3. 多线程测试

void Worker(int id)
{
    for (int i = 0; i < 5; ++i)
    {
        Logger::Instance().Log("Thread " + std::to_string(id) + " message " + std::to_string(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

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;
}

运行结果示例(行号随机):

[1] Thread 1 message 0
[2] Thread 2 message 0
[3] Thread 3 message 0
[4] Thread 1 message 1
[5] Thread 2 message 1
[6] Thread 3 message 1
...

可以看到,所有线程共享同一个 Logger 实例,日志输出顺序由互斥量控制。


4. 进一步优化

  1. 使用 std::unique_ptr

    • 替换裸指针 instance_,让析构自动管理内存,避免泄漏。
  2. 懒加载 + 智能指针

    static std::unique_ptr <Logger> instance_;
    static std::once_flag initFlag_;
    static Logger& Instance()
    {
        std::call_once(initFlag_, [](){
            instance_.reset(new Logger());
        });
        return *instance_;
    }
  3. 双重检查锁

    • 在 C++11 之前的老代码中常见,现已不推荐。
  4. 使用 std::shared_ptr 进行多实例引用

    • 如果业务需要对单例对象进行引用计数,可以改为 std::shared_ptr
  5. 日志级别与文件切分

    • Log 中加入日志级别判断,将日志写入不同文件,使用 std::ofstream

5. 常见误区

误区 说明
认为 static 局部变量已经线程安全 C++11 才保证局部静态变量的初始化线程安全,老版本需要手动同步。
只用 std::once_flag 但忘记删除拷贝 如果拷贝构造未删除,外部可能创建多个实例导致单例失效。
直接使用裸指针 易导致内存泄漏,尤其在程序退出前需要手动删除。

6. 小结

通过 std::call_oncestd::once_flag,C++11 之后可以轻松实现线程安全的懒汉式单例。核心思路是一次性执行初始化操作,随后所有线程共享同一实例。结合互斥量控制成员函数的并发访问,即可构建高效、可靠的单例对象。

题外话:如果你想进一步学习 C++ 并发编程,建议阅读《C++ Concurrency in Action》与《Effective Modern C++》等经典书籍。祝编码愉快!

发表评论