Подтвердить что ты не робот

Синглтон с многопоточными

Этот вопрос задавали в интервью. Первая часть заключалась в написании одноэлементного класса:

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();
    }
};

то мне сказали, что, хотя требуется мьютекс, ожидающий и отправляющий мьютекс неэффективен, так как требуется время. И есть лучший способ справиться с этой ситуацией.

Кто-нибудь знает более эффективный способ обработки одиночного класса в многопоточной ситуации?

4b9b3361

Ответ 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);
}