Дважды проверил Lock Singleton в C ++ 11 - PullRequest
43 голосов
/ 22 мая 2011

Свободна ли следующая гонка данных для одной реализации?

static std::atomic<Tp *> m_instance;
...

static Tp &
instance()
{
    if (!m_instance.load(std::memory_order_relaxed))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!m_instance.load(std::memory_order_acquire))
        {
            Tp * i = new Tp;
            m_instance.store(i, std::memory_order_release);    
        }    
    }

    return * m_instance.load(std::memory_order_relaxed);
}

Является ли std::memory_model_acquire операции загрузки излишним?Можно ли еще больше ослабить операции загрузки и хранения, переключив их на std::memory_order_relaxed?В таком случае, достаточно ли семантики получения / выпуска std::mutex, чтобы гарантировать ее правильность, или дополнительно требуется std::atomic_thread_fence(std::memory_order_release), чтобы гарантировать, что записи в память конструктора будут происходить до расслабленного хранилища?Тем не менее, является ли использование забора эквивалентным, чтобы иметь хранилище с memory_order_release?

РЕДАКТИРОВАТЬ : Благодаря ответу Джона я придумал следующую реализацию, которая должна бытьРаса свободна.Хотя внутренняя нагрузка может быть вообще не атомарной, я решил оставить расслабленную нагрузку, поскольку она не влияет на производительность.По сравнению с тем, чтобы всегда иметь внешнюю нагрузку с порядком получения памяти, механизм thread_local повышает производительность доступа к экземпляру примерно на порядок.

static Tp &
instance()
{
    static thread_local Tp *instance;

    if (!instance && 
        !(instance = m_instance.load(std::memory_order_acquire)))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!(instance = m_instance.load(std::memory_order_relaxed)))
        {
            instance = new Tp; 
            m_instance.store(instance, std::memory_order_release);    
        }    
    }
    return *instance;
}

Ответы [ 3 ]

27 голосов
/ 23 мая 2011

Я думаю, что это отличный вопрос, и у Джона Калсбека правильный ответ.

Однако, для ясности, ленивый синглтон лучше всего реализовать с использованием классического синглтона Мейерса. Он гарантировал правильную семантику в C ++ 11.

§ 6.7.4

... Если контроль входит объявление одновременно во время инициализации переменной, параллельное выполнение должно ожидать завершение инициализации. ...

Синглтон Мейера предпочтителен в том смысле, что компилятор может агрессивно оптимизировать параллельный код. Компилятор был бы более ограничен, если бы ему пришлось сохранять семантику std::mutex. Кроме того, синглтон Мейера составляет 2 строки , и практически невозможно ошибиться.

Вот классический пример синглтона Мейера. Простой, элегантный и сломанный в C ++ 03. Но простой, элегантный и мощный в C ++ 11.

class Foo
{
public:
   static Foo& instance( void )
   {
      static Foo s_instance;
      return s_instance;
   }
};
20 голосов
/ 22 мая 2011

Эта реализация не свободна от гонки.Атомарное хранилище синглтона, хотя и использует семантику выпуска, будет синхронизироваться только с операцией получения совпадения, то есть с операцией загрузки, которая уже защищена мьютексом.

Возможно, внешняя расслабленная нагрузка будетпрочитайте ненулевой указатель до того, как поток блокировки завершит инициализацию синглтона.

С другой стороны, захват, который защищен блокировкой, является избыточным.Он будет синхронизироваться с любым хранилищем с семантикой выпуска в другом потоке, но в этот момент (благодаря мьютексу) единственный поток, который может сохранить, это текущий поток.Эта нагрузка даже не должна быть атомарной - никакие хранилища не могут происходить из другого потока.

См. Серия Энтони Уильямса о многопоточности C ++ 0x .

7 голосов
/ 20 ноября 2011

См. Также call_once . Там, где вы ранее использовали синглтон для чего-то, но на самом деле не использовали возвращаемый объект для чего-либо, call_once может быть лучшим решением. Для обычного синглтона вы могли бы сделать call_once, чтобы установить (глобальную?) Переменную, а затем вернуть эту переменную ...

Упрощенно для краткости:

template< class Function, class... Args>
void call_once( std::once_flag& flag, Function&& f, Args&& args...);
  • Выполняется ровно одно выполнение ровно одной из функций, переданных как f вызовам в группе (тот же объект флага) .

  • Никакой вызов в группе не возвращается до успешного завершения вышеупомянутого выполнения выбранной функции

...