Дважды проверил Lock Singleton в C++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
?
РЕДАКТИРОВАТЬ: Благодаря ответу Джона, я придумал следующую реализацию, которая должна быть свободна от гонки данных. Несмотря на то, что внутренняя нагрузка вообще может быть неатомарной, я решил оставить расслабленную нагрузку, поскольку она не влияет на производительность. По сравнению с тем, чтобы всегда иметь внешнюю нагрузку с порядком получения памяти, механизм 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 ответа
Эта реализация не свободна от гонок. Атомарное хранилище синглтона, хотя и использует семантику выпуска, будет синхронизироваться только с операцией получения соответствия, то есть с операцией загрузки, которая уже защищена мьютексом.
Возможно, что внешняя расслабленная нагрузка прочитает ненулевой указатель, прежде чем блокирующий поток завершит инициализацию синглтона.
Приобретение, которое защищено замком, с другой стороны, является избыточным. Он будет синхронизироваться с любым хранилищем с семантикой выпуска в другом потоке, но в этот момент (благодаря мьютексу) единственный поток, который может сохранить, это текущий поток. Эта нагрузка даже не должна быть атомарной - никакие хранилища не могут происходить из другого потока.
Я думаю, что это отличный вопрос, и у Джона Калсбека правильный ответ.
Однако для ясности ленивый синглтон лучше всего реализовать с использованием классического синглтона Meyers. Он гарантировал правильную семантику в 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;
}
};
Смотрите также call_once. Там, где вы ранее использовали что-то одно, но на самом деле не использовали возвращаемый объект для чего-либо, call_once может быть лучшим решением. Для обычного синглтона вы могли бы сделать call_once, чтобы установить (глобальную?) Переменную, а затем вернуть эту переменную...
Упрощенно для краткости:
template< class Function, class... Args>
void call_once( std::once_flag& flag, Function&& f, Args&& args...);
Выполняется ровно одно выполнение ровно одной из функций, переданных как f вызовам в группе (тот же объект-флаг).
Вызов в группе не возвращается до успешного завершения вышеупомянутого выполнения выбранной функции.