文章目录
- Singleton
-
- [1 指针版本](#1 指针版本)
-
- [Version 1 非线程安全版本](#Version 1 非线程安全版本)
- [Version 2 加锁版本](#Version 2 加锁版本)
- [Version 3.1 双重检查锁版本 Atomic+Mutex](#Version 3.1 双重检查锁版本 Atomic+Mutex)
- [Version 3.2 双重检查锁版本 Atomic-only](#Version 3.2 双重检查锁版本 Atomic-only)
- [Version 3 两种方式对比](#Version 3 两种方式对比)
- [2 引用版本](#2 引用版本)
-
- [Version 1 简单版本 不推荐](#Version 1 简单版本 不推荐)
- [Version 2 初始化安全版本](#Version 2 初始化安全版本)
- [Version 3 初始化+操作安全版本](#Version 3 初始化+操作安全版本)
- Explanation
- Comparison
Singleton
1 指针版本
Version 1 非线程安全版本
cpp
class Logger {
public:
static Logger *GetInstance() {
if (instance == nullptr) {
instance = new Logger();
}
return instance;
}
void Log(const std::string &message) {
std::cout << message << std::endl;
}
private:
static Logger *instance;
Logger() {}
};
Logger *Logger::instance = nullptr;
Version 2 加锁版本
增加锁,用于保证线程安全,但是锁开销会影响性能。
cpp
class Logger {
public:
static Logger *GetInstance() {
std::lock_guard<std::mutex> lk(mutex_);
if (instance == nullptr) {
instance = new Logger();
}
return instance;
}
void Log(const std::string &message) {
std::cout << message << std::endl;
}
private:
Logger() {}
static Logger *instance;
static std::mutex mutex_;
};
Logger *Logger::instance = nullptr;
std::mutex Logger::mutex_;
Version 3.1 双重检查锁版本 Atomic+Mutex
cpp
class Logger {
public:
static Logger* GetInstance() {
// First, attempt to load the current instance atomically
Logger* tmp = instance.load(std::memory_order_acquire);
// If the instance is nullptr, create it
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx); // Lock only during initialization
tmp = instance.load(std::memory_order_relaxed); // Check again inside the lock
if (tmp == nullptr) {
tmp = new Logger(); // Create a new instance
instance.store(tmp, std::memory_order_release); // Atomically set the instance
}
}
return tmp;
}
void Log(const std::string& message) {
std::cout << message << std::endl;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() {} // Private constructor to prevent direct instantiation
static std::atomic<Logger*> instance; // Atomic pointer to the Singleton instance
static std::mutex mtx; // Mutex to protect initialization
};
// Initialize the atomic pointer and mutex
std::atomic<Logger*> Logger::instance(nullptr);
std::mutex Logger::mtx;
Version 3.2 双重检查锁版本 Atomic-only
cpp
class Logger {
public:
static Logger* GetInstance() {
// First, attempt to load the current instance atomically
Logger* tmp = instance.load(std::memory_order_acquire);
// If the instance is nullptr, create it
if (tmp == nullptr) {
tmp = new Logger(); // Create a new instance
// Atomically set the instance if no other thread has done so
if (!instance.compare_exchange_strong(tmp, tmp)) {
delete tmp; // Another thread won the race, delete the temporary instance
tmp = instance.load(std::memory_order_acquire); // Reload the instance
}
}
return tmp;
}
void Log(const std::string& message) {
std::cout << message << std::endl;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() {} // Private constructor to prevent direct instantiation
static std::atomic<Logger*> instance; // Atomic pointer to the Singleton instance
};
// Initialize the atomic pointer to nullptr
std::atomic<Logger*> Logger::instance(nullptr);
Version 3 两种方式对比
-
Only Atomic:
- Atomic Check: We first check the
instance
atomically withinstance.load
. If it'snullptr
, we attempt to create the instance usingnew
. - Atomic Set: We use
compare_exchange_strong
to ensure that only one thread creates the instance. If another thread has already created the instance, it returns the existing one. - No Mutex: There is no mutex involved here. The atomic operations ensure thread safety during the initialization phase.
- Atomic Check: We first check the
-
Atomic + Mutex:
- Atomic First Check: The first check of the
instance
pointer is atomic usinginstance.load
. - Mutex Locking: If the instance is
nullptr
, we lock a mutex (std::mutex mtx
) to synchronize access during the actual creation of the instance. - Double Check Inside Lock: After acquiring the mutex, we perform another check of the
instance
. This prevents other threads from creating multiple instances if they were waiting on the mutex. - Atomic Set: We use
instance.store
to atomically set theinstance
pointer once it's initialized.
- Atomic First Check: The first check of the
- Comparison of Effectiveness:
Factor | Atomic-only | Atomic + Mutex |
---|---|---|
Initialization | Atomic operations ensure safe initialization. | Mutex ensures exclusive access during initialization. |
Post-Initialization Access | Lock-free after initialization. | Mutex locking still required to access instance. |
Performance (High Concurrency) | High performance: No lock contention after init. | Slower due to mutex locking, even after initialization. |
Scalability (Concurrency) | Highly scalable: No locks post-initialization. | Less scalable: Mutex lock can cause contention. |
Memory Consistency | Ensured via atomic operations and memory_order_acquire/release . |
Ensured by std::mutex for synchronization. |
Simplicity | Slightly more complex due to atomic operations. | Simpler for developers familiar with mutexes. |
-
Atomic-only approach is more effective in high-concurrency environments, especially when you expect many threads accessing the Singleton. Since the initialization is thread-safe and lock-free after the instance is created, it scales much better than the mutex-based approach.
-
Atomic + Mutex approach might be easier to understand for developers familiar with mutexes and might work well in lower-concurrency environments. However, the mutex adds overhead for each access, and if the program has many threads, it will result in contention and slower performance.
-
If you are building a highly concurrent system, prefer the atomic-only approach, as it will perform better with minimal locking overhead.
-
If you have a simpler, lower-concurrency application, using atomic + mutex might be a good trade-off because it provides simplicity and guarantees correct initialization with easy-to-understand synchronization.
2 引用版本
Version 1 简单版本 不推荐
cpp
class Logger {
public:
static Logger &GetInstance() {
return instance;
}
void Log(const std::string &message) {
std::cout << message << std::endl;
}
private:
static Logger instance;
Logger() {
}
};
Logger Logger::instance;
Version 2 初始化安全版本
c++机制保证初始化安全
cpp
class Logger {
public:
static Logger& GetInstance() {
static Logger instance;
return instance;
}
void Log(const std::string &message) {
std::cout << message << std::endl;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() {}
};
Version 3 初始化+操作安全版本
增加操作安全
cpp
class Logger {
public:
static Logger &GetInstance() {
static Logger instance;
return instance;
}
void Log(const std::string &message) {
std::lock_guard<std::mutex> lk(mtx);
std::cout << message << std::endl;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() {}
std::mutex mtx;
};
Explanation
初始化过程线程安全原因:
-
Static Local Variable:
-
In the
GetInstance()
method, we declare a static local variableinstance
.static Logger instance;
ensures thatinstance
is only created once and persists for the entire lifetime of the program.
-
-
First-Time Initialization:
- The first time
GetInstance()
is called, the static variableinstance
is initialized. This is where the thread-safety comes into play. The C++11 standard guarantees that the initialization of a static local variable will be thread-safe. - If multiple threads try to call
GetInstance()
simultaneously, only one thread will initialize theinstance
. The other threads will wait until the initialization is complete, and then they will all see the same instance when they callGetInstance()
again.
- The first time
-
Thread-Safe Static Initialization:
- The C++11 guarantee ensures that even if multiple threads try to initialize the
instance
simultaneously, the static variable will only be initialized once. The other threads will see the already initialized object, which eliminates any race condition.
- The C++11 guarantee ensures that even if multiple threads try to initialize the
-
Post-Initialization Access:
- After initialization, the reference
instance
is ready for access, and since it is a static variable, it is always available. There is no locking required for accessinginstance
after it is initialized, making access very efficient.
- After initialization, the reference
-
No Mutex or Atomic Operations:
- Since the C++ standard guarantees thread-safe initialization of static local variables, there is no need for additional synchronization mechanisms such as mutexes or atomic operations. The instance is initialized only once, and once it is initialized, it is ready for fast, lock-free access.
Comparison
Factor | Atomic-only Singleton | Atomic + Mutex Singleton | Reference Singleton |
---|---|---|---|
Thread-Safe Initialization | Thread-safe initialization using atomic operations. | Thread-safe initialization using atomic + mutex locking. | Guaranteed thread-safe initialization due to static storage duration in C++11. |
Memory Management | Requires dynamic memory allocation (using new ). |
Requires dynamic memory allocation (using new ). |
No dynamic memory allocation; the instance is static. |
Post-Initialization Access | Lock-free after initialization, very fast. | Mutex still required for each access. | Lock-free after initialization, very fast. |
Performance (High Concurrency) | Very high performance due to lock-free access. | Lower performance due to mutex lock overhead. | Very high performance with no locking or atomic ops. |
Scalability (Concurrency) | Highly scalable with minimal contention. | Less scalable due to mutex contention. | Highly scalable since there's no contention. |
Simplicity | More complex, requires understanding of atomic operations. | More complex due to mutex usage and atomic operations. | Simpler and more straightforward. |
Memory Usage | Requires dynamic memory allocation for the Singleton. | Requires dynamic memory allocation for the Singleton. | No dynamic memory allocation, very efficient. |
Lifetime Management | Requires manual cleanup or reliance on smart pointers. | Requires manual cleanup or reliance on smart pointers. | Managed automatically by the compiler with static duration. |
Safety | Thread-safe, but requires careful handling of atomic ops. | Thread-safe, but introduces locking overhead. | Thread-safe due to the C++ static initialization guarantee, no locking needed. |
Use Case | Suitable for high-concurrency, dynamic memory applications where you need to fine-tune memory allocation. | Suitable for high-concurrency, but mutex introduces some overhead in high-load |