Double-Checked Lock Singleton в С++ 11

Являются ли следующие данные о гонке реализации однопользовательской версии?

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?

EDIT: благодаря ответу Джона я придумал следующую реализацию, которая должна быть бесплатной для рассылки данных. Несмотря на то, что внутренняя нагрузка может быть неатомной вообще, я решил оставить расслабленную нагрузку, поскольку она не влияет на производительность. По сравнению с всегда имеющей внешнюю нагрузку с порядком получения памяти, механизм 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;
}

Ответ 1

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

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

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

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

Ответ 2

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

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

§ 6.7.4

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

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

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

class Foo
{
public:
   static Foo& instance( void )
   {
      static Foo s_instance;
      return s_instance;
   }
};

Ответ 3

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

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

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

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