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

在多线程环境下,单例模式的实现需要确保只有一个实例被创建,并且在并发访问时不会出现竞争条件。下面以 C++17 为例,演示几种常见的线程安全实现方式,并说明各自的优缺点。

1. C++11 的 std::call_once

最直接、最推荐的方式是使用 std::call_oncestd::once_flag。它们是标准库提供的原子操作,天然线程安全,且在第一次调用时才会初始化。

#include <iostream>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() { instancePtr_ = new Singleton(); });
        return *instancePtr_;
    }

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

    void sayHello() const { std::cout << "Hello from Singleton\n"; }

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

    static Singleton* instancePtr_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;

优点

  • 简洁:代码量少,易于维护。
  • 高效:只有第一次调用会触发一次性初始化,后续调用几乎不消耗资源。
  • 标准:属于 C++ 标准库,保证在所有标准实现上的行为一致。

缺点

  • 需要手动管理单例的销毁,如果不使用 std::unique_ptrstd::shared_ptr,程序退出时会留下悬挂的静态对象,可能导致析构顺序问题。

2. 局部静态变量(Meyers Singleton)

C++11 之后,局部静态变量的初始化已被保证为线程安全。只需在函数内部声明静态对象即可。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // 线程安全初始化
        return instance;
    }

    // ...
};

优点

  • 最简洁:几乎不需要任何额外代码。
  • 自动销毁:静态对象在程序结束时按正确顺序析构。

缺点

  • 延迟初始化:如果 instance() 从未被调用,单例永不构造。
  • 难以控制:无法显式控制实例的生命周期,例如提前销毁或重建。

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

传统的双重检查锁方案在 C++11 之前不安全,但使用 std::atomic 后可以工作。示例代码:

#include <atomic>
#include <mutex>

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

    // ...
private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;

优点

  • 延迟初始化:首次访问时才构造。
  • 性能:在已初始化后,无需加锁即可获取实例。

缺点

  • 实现繁琐:需要仔细处理内存序和锁,错误可能导致难以调试的并发 bug。
  • 不推荐std::call_once 更加直观且已被标准化。

4. 线程局部单例(Thread-Local Singleton)

有时你需要为每个线程维护一个独立的单例实例。可以使用 thread_local 关键字。

class ThreadSingleton {
public:
    static ThreadSingleton& instance() {
        thread_local ThreadSingleton instance;
        return instance;
    }

    // ...
};

适用场景

  • 需要线程级别的隔离,例如线程安全的日志记录器。
  • 不想共享状态导致竞争的场景。

小结

  • 推荐:使用 std::call_once + std::once_flag 或者局部静态变量(Meyers)实现单例。
  • 不推荐:双重检查锁需要细致的原子操作,容易出错。
  • 特殊需求:如果每个线程需要独立实例,考虑 thread_local

下面给出一个完整示例,演示在多线程环境下安全获取单例,并记录线程 ID:

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

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

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[Thread " << std::this_thread::get_id() << "] " << msg << '\n';
    }

private:
    Logger() = default;
    ~Logger() = default;
    std::mutex mutex_;
};

void worker(int id) {
    for (int i = 0; i < 5; ++i) {
        Logger::instance().log("Worker " + std::to_string(id) + " iteration " + std::to_string(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    const int numThreads = 4;
    std::vector<std::thread> threads;
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) t.join();
    return 0;
}

运行结果显示,所有线程共享同一个 Logger 实例,输出顺序不确定,但线程 ID 一致,证明单例在多线程环境下的正确性。

发表评论