在多线程环境中,单例模式需要确保只创建一次实例且线程安全。下面介绍在C++17/20中实现双重检查锁(Double-Checked Locking)的一种可靠方式,并解释其细节。
1. 经典实现的缺陷
传统的双重检查锁实现是:
class Singleton {
public:
static Singleton& instance() {
if (!ptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (!ptr) { // 第二次检查
ptr = new Singleton();
}
}
return *ptr;
}
private:
Singleton() = default;
static Singleton* ptr;
static std::mutex mtx;
};
在C++98/03中,这种实现存在内存屏障和编译器重排序问题:new Singleton() 的写入可能在 ptr 的赋值之前被重排序,导致其他线程看到未初始化的对象。
2. C++ 的内存模型解决方案
C++11 起,std::atomic 和 std::memory_order 提供了对内存顺序的精确控制。可以改用 std::atomic<Singleton*> 并使用 memory_order_acquire/release。
#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(mtx_);
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 mtx_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mtx_;
关键点解释
instance_是原子指针,保证对它的读写是原子操作。load(std::memory_order_acquire):当读取到非空指针时,后续所有操作必须在此之前完成。store(std::memory_order_release):写入指针时,前面的所有操作必须在此之前完成。std::memory_order_relaxed用于在加锁内部的再次检查,因为此时锁已保证原子性。
3. 更简洁的 C++17 方案
C++17 的 std::call_once 与 std::once_flag 本身就是线程安全的单例初始化工具。
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []{
instance_ = new Singleton();
});
return *instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
call_once确保 lambda 只被执行一次,且对所有线程可见。- 该实现避免了手动使用
std::atomic,更易读、易维护。
4. 对象销毁
在多线程程序结束时,单例对象可能需要被销毁。
- 采用
std::unique_ptr或者std::shared_ptr并配合std::call_once进行销毁。 - 或者利用程序退出时的静态对象析构顺序(在
main结束后自动销毁)。
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []{
instance_.reset(new Singleton());
});
return *instance_;
}
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;
5. 小结
- 对于 C++11+,建议使用
std::atomic+memory_order或std::call_once,两者都能安全实现单例。 std::call_once更易读,且内部已处理所有同步细节,适合大多数场景。- 若需延迟初始化且不想使用
std::call_once,可使用std::atomic并严格控制内存顺序。
通过上述方法,你可以在 C++ 中实现既安全又高效的线程安全单例。