C++ 中的多线程安全单例模式实现与实践

在 C++ 开发中,单例模式(Singleton)常被用于实现全局共享资源,例如日志记录器、配置管理器等。随着多线程程序的普及,单例的线程安全实现成为关键点。下面我们将从多种实现方式进行对比,说明它们的优缺点,并给出实战代码示例。


1. 传统懒汉式(带锁)

class Logger {
public:
    static Logger& getInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!instance_) {
            instance_ = new Logger();
        }
        return *instance_;
    }

private:
    Logger() = default;
    ~Logger() = default;

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    static Logger* instance_;
    static std::mutex mutex_;
};

Logger* Logger::instance_ = nullptr;
std::mutex Logger::mutex_;
  • 优点:实现直观,所有线程都能保证安全。
  • 缺点:每次访问都需要获取互斥锁,导致性能下降。且必须手动管理单例生命周期,容易出现内存泄漏。

2. 饿汉式(编译期构造)

class Config {
public:
    static Config& getInstance() {
        return instance_;
    }

private:
    Config() = default;
    ~Config() = default;
    Config(const Config&) = delete;
    Config& operator=(const Config&) = delete;

    static Config instance_;
};

Config Config::instance_;
  • 优点:无需锁,线程安全;实例在程序启动时就创建。
  • 缺点:如果单例使用量不高,导致资源在程序未用到时就被分配,且不可延迟销毁。

3. C++11 std::call_once + std::once_flag

class DBConnection {
public:
    static DBConnection& getInstance() {
        std::call_once(flag_, [](){ instance_.reset(new DBConnection()); });
        return *instance_;
    }

private:
    DBConnection() = default;
    ~DBConnection() = default;
    DBConnection(const DBConnection&) = delete;
    DBConnection& operator=(const DBConnection&) = delete;

    static std::unique_ptr <DBConnection> instance_;
    static std::once_flag flag_;
};

std::unique_ptr <DBConnection> DBConnection::instance_;
std::once_flag DBConnection::flag_;
  • 优点:懒加载、线程安全、代码简洁。std::call_once 确保一次性初始化,后续访问无需锁。
  • 缺点std::unique_ptr 的销毁时机取决于程序结束,若需要在特定时间销毁,需自行控制。

4. Meyer’s 单例(局部静态变量)

class Cache {
public:
    static Cache& getInstance() {
        static Cache instance;   // C++11 之后,初始化是线程安全的
        return instance;
    }

private:
    Cache() = default;
    ~Cache() = default;
    Cache(const Cache&) = delete;
    Cache& operator=(const Cache&) = delete;
};
  • 优点:实现最简洁,编译器保证线程安全(C++11 之后)。实例在第一次调用 getInstance() 时才创建,后续无需锁。
  • 缺点:C++11 标准保证线程安全,但在某些极端多线程场景(如多进程或在异常中初始化)仍需注意。

5. 延迟销毁与智能指针

如果单例需要在程序运行期间多次创建/销毁,可结合 std::shared_ptrstd::weak_ptr 实现:

class Service {
public:
    static std::shared_ptr <Service> getInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!instance_) {
            instance_ = std::shared_ptr <Service>(new Service());
        }
        return instance_;
    }

    static void release() {
        std::lock_guard<std::mutex> lock(mutex_);
        instance_.reset();
    }

private:
    Service() = default;
    ~Service() = default;

    static std::shared_ptr <Service> instance_;
    static std::mutex mutex_;
};

std::shared_ptr <Service> Service::instance_;
std::mutex Service::mutex_;
  • 优点:可以显式释放单例,适用于资源占用较大的对象。
  • 缺点:每次 getInstance() 仍需锁,性能稍低。

6. 代码实践:多线程日志记录

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

class Logger {
public:
    static Logger& getInstance() {
        static Logger instance;          // Meyer's 单例
        return instance;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        outfile_ << msg << std::endl;
    }

private:
    Logger() {
        outfile_.open("app.log", std::ios::app);
    }
    ~Logger() { outfile_.close(); }

    std::ofstream outfile_;
    std::mutex mutex_;
};

void worker(int id) {
    for (int i = 0; i < 5; ++i) {
        Logger::getInstance().log("Thread " + std::to_string(id) + " msg " + 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);
    t1.join();
    t2.join();
    return 0;
}
  • 说明Logger 使用 Meyer’s 单例实现,文件写入在 log() 内部加锁,确保线程安全。
  • 扩展:可以加入日志等级、滚动日志文件等功能。

7. 结语

  • 选择何种实现?

    • 若需 懒加载性能敏感,推荐 std::call_once 或 Meyer’s 单例。
    • 若需要 可延迟销毁,使用 std::shared_ptr + std::mutex
    • 对于不需要懒加载的情况,可直接采用 饿汉式
  • 注意事项

    • 线程安全不等于无锁;正确使用 std::mutexstd::lock_guard 可以保持安全与简洁。
    • 在使用单例时,需评估其生命周期与资源占用,避免单例造成的 资源泄漏延迟销毁 产生的内存压力。

通过上述多种实现方式,开发者可以根据具体需求挑选最合适的单例模式,并在多线程环境中安全高效地共享资源。祝你编码愉快!

发表评论