Синглтон с многопоточными
Этот вопрос задавали в интервью. Первая часть заключалась в написании одноэлементного класса:
class Singleton
{
static Singleton *singletonInstance;
Singleton() {}
public:
static Singleton* getSingletonInstance()
{
if(singletonInstance == null)
{
singletonInstance = new Singleton();
}
return singletonInstance;
}
};
Затем меня спросили, как обращаться с этим getSingletonInstance() в многопоточной ситуации. Я не был уверен, но я изменил как:
class Singleton
{
static Singleton *singletonInstance;
Singleton() {}
static mutex m_;
public:
static Singleton* getSingletonInstance()
{
m_pend();
if(singletonInstance == null)
{
singletonInstance = new Singleton();
}
return singletonInstance;
}
static void releaseSingleton()
{
m_post();
}
};
то мне сказали, что, хотя требуется мьютекс, ожидающий и отправляющий мьютекс неэффективен, так как требуется время. И есть лучший способ справиться с этой ситуацией.
Кто-нибудь знает более эффективный способ обработки одиночного класса в многопоточной ситуации?
Ответы
Ответ 1
В С++ 11 гарантируется выполнение инициализации с потоком:
static Singleton* getSingletonInstance()
{
static Singleton instance;
return &instance;
}
В С++ 03 общий подход заключался в использовании блокировки с двойной проверкой; проверяя флаг (или сам указатель), чтобы увидеть, может ли объект быть неинициализирован, и только блокировка мьютекса, если это возможно. Для этого требуется какой-то нестандартный способ атомарного считывания указателя (или связанного с ним логического флага); многие реализации неправильно используют простой указатель или bool
, не гарантируя, что изменения на одном процессоре видны на других. Код может выглядеть примерно так, хотя я почти наверняка получил что-то не так:
static Singleton* getSingletonInstance()
{
if (!atomic_read(singletonInstance)) {
mutex_lock lock(mutex);
if (!atomic_read(singletonInstance)) {
atomic_write(singletonInstance, new Singleton);
}
}
return singletonInstance;
}
Это довольно сложно сделать правильно, поэтому я предлагаю вам не беспокоиться. В С++ 11 вы можете использовать стандартные атомарные и мьютексы, если по какой-то причине вы хотите сохранить динамическое распределение вашего примера.
Обратите внимание, что я говорю только о синхронизированной инициализации, а не о синхронизированном доступе к объекту (который предоставляется вашей версией, блокируя мьютекс в аксессуре и освобождая его позже через отдельную функцию). Если вам нужен замок для безопасного доступа к самому объекту, вы, очевидно, не сможете избежать блокировки при каждом доступе.
Ответ 2
Как предложил @piokuc, вы также можете использовать функцию once. Если у вас есть С++ 11:
#include <mutex>
static void init_singleton() {
singletonInstance = new Singleton;
}
static std::once_flag singleton_flag;
Singleton* getSingletonInstance() {
std::call_once(singleton_flag, init_singleton);
return singletonInstance;
}
И да, это будет разумно работать, если new Singleton
выдает исключение.
Ответ 3
Если у вас есть С++ 11, вы можете сделать singletonInstance
атомную переменную, затем используйте блокировку с двойным контролем:
if (singletonInstance == NULL) {
lock the mutex
if (singletonInstance == NULL) {
singletonInstance = new Singleton;
}
unlock the mutex
}
return singletonInstance;
Ответ 4
Фактически вы должны заблокировать синглтон, а не экземпляр. Если экземпляр требует блокировки, это должно обрабатываться вызывающим (или, возможно, самим экземпляром, в зависимости от того, какой интерфейс он предоставляет)
Обновить пример кода:
#include <mutex>
class Singleton
{
static Singleton *singletonInstance;
Singleton() {}
static std::mutex m_;
public:
static Singleton* getSingletonInstance()
{
std::lock_guard<std::mutex> lock(m_);
if(singletonInstance == nullptr)
{
singletonInstance = new Singleton();
}
return singletonInstance;
}
}
Ответ 5
Если вы используете потоки POSIX, вы можете использовать вещи pthread_once_t
и pthread_key_t
, таким образом вы можете вообще избегать использования мьютексов. Например:
template<class T> class ThreadSingleton : private NonCopyable {
public:
ThreadSingleton();
~ThreadSingleton();
static T& instance();
private:
ThreadSingleton( const ThreadSingleton& );
const ThreadSingleton& operator=( const ThreadSingleton& )
static pthread_once_t once_;
static pthread_key_t key_;
static void init(void);
static void cleanUp(void*);
};
И реализация:
template<class T> pthread_once_t ThreadSingleton<T>::once_ = PTHREAD_ONCE_INIT;
template<class T> pthread_key_t ThreadSingleton<T>::key_;
template<class T>
T& ThreadSingleton<T>::instance()
{
pthread_once(&once_,init);
T* value = (T*)pthread_getspecific(key_);
if(!value)
{
value = new T();
pthread_setspecific(key_,value);
}
return *value;
}
template<class T> void ThreadSingleton<T>::cleanUp(void* data)
{
delete (T*)data;
pthread_setspecific(key_,0);
}
template<class T> void ThreadSingleton<T>::init()
{
pthread_key_create(&key_,cleanUp);
}