Поточно-безопасная инициализация локальных статических константных объектов функции

Этот вопрос заставили меня усомниться в практике, которой я следовал годами.

Для потокобезопасной инициализации локальных статических константных объектов, я защищаю фактическую конструкцию объекта, но не инициализацию локальной ссылки на функцию имея в виду именно это. Что-то вроде этого:

namespace {
  const some_type& create_const_thingy()
  {
     lock my_lock(some_mutex);
     static const some_type the_const_thingy;
     return the_const_thingy;
  }
}

void use_const_thingy()
{
  static const some_type& the_const_thingy = create_const_thingy();

  // use the_const_thingy

}

Идея в том, что блокировка требует времени, и если ссылка будет перезаписана несколькими потоками, это не имеет значения.

Мне было бы интересно, если это

  1. достаточно безопасно на практике?
  2. безопасно в соответствии с Правилами? (Я знаю, текущий стандарт даже не знает, что такое «параллелизм», но как насчет того, чтобы попирать уже инициализированную ссылку? И есть ли в других стандартах, таких как POSIX, что-то связанное с этим?)

Причина, по которой я хочу это знать, заключается в том, что я хочу знать, могу ли я оставить код как есть или мне нужно вернуться и исправить это.


Для пытливых умов:

Многие из таких локальных статических константных объектов, которые я использовал, представляют собой карты, которые инициализируются из константных массивов при первом использовании и используются для поиска. Например, у меня есть несколько анализаторов XML, в которых строки имен тегов сопоставляются со значениями enum, так что позже я смогу switch использовать enum значения тегов.


Поскольку я получил несколько ответов о том, что делать вместо этого, но не получил ответа на мои актуальные вопросы (см. 1. и 2. выше), я начну вознаграждение за это. Еще раз:
Меня не интересует, что я могу сделать вместо, я действительно хочу знать об этом.


person sbi    schedule 02.06.2010    source источник
comment
Я не понимаю, насколько ваш вопрос существенно отличается от того, на который вы ссылаетесь. И разве это не задавалось много раз раньше в той или иной форме? Например, stackoverflow. .com / questions / 1270927 /.   -  person    schedule 02.06.2010
comment
@Neil: Я не спрашиваю о блокировке с двойной проверкой и т. Д. В целом, но конкретно о том, чтобы не защищать назначение простого адреса. Я не нашел ничего по этому поводу, но я был бы с радостью отнесен к этому, если он существует здесь.   -  person sbi    schedule 02.06.2010


Ответы (8)


Это моя вторая попытка ответить. Я отвечу только на первый из ваших вопросов:

  1. достаточно безопасно на практике?

Нет. Как вы утверждаете, вы только обеспечиваете защиту создания объекта, а не инициализацию ссылки на объект.

В отсутствие модели памяти C ++ 98 и явных заявлений от поставщика компилятора нет никаких гарантий, что запись в память, представляющая фактическую ссылку, и запись в память, которая содержит значение флага инициализации (если это как это реализовано) для ссылки видны в одном и том же порядке из нескольких потоков.

Как вы также говорите, перезапись ссылки несколько раз одним и тем же значением не должна иметь смыслового различия (даже при наличии разрывов слов, что обычно маловероятно и, возможно, даже невозможно для вашей архитектуры процессора), но есть один случай, когда это имеет значение: < em> Когда несколько потоков спешат вызвать функцию в первый раз во время выполнения программы. В этом случае один или несколько из этих потоков могут увидеть установленный флаг инициализации до инициализации фактической ссылки.

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

person rjnilsson    schedule 22.06.2010
comment
Вы действительно предполагаете, что могут быть реализации, которые сначала устанавливают флаг инициализации, а затем назначают указатель? Изменить: Ой, подождите, хорошо. Базовая платформа может переупорядочить записи, если ей это понравится! Это действительно так. Я вообще не думал об этом, но это действительно ужасно. Если никто не придет и не покажет ошибку в этом аргументе, я ее приму. - person sbi; 22.06.2010
comment
@sbi: Я даже не думаю, что записи придется переупорядочивать аппаратно; ссылка и флаг могут быть разделены в памяти достаточно, чтобы находиться в разных строках кэша. - person rjnilsson; 22.06.2010
comment
Да, это тоже может быть проблемой. Спасибо, что ответили на мой вопрос! - person sbi; 23.06.2010
comment
Двойная проверка блокировки на самом деле небезопасна. - person Puppy; 25.01.2012

Вот мой вариант (если вы действительно не можете инициализировать его до запуска потоков):

Я видел (и использовал) что-то подобное для защиты статической инициализации с помощью boost :: once

#include <boost/thread/once.hpp>

boost::once_flag flag;

// get thingy
const Thingy & get()
{
    static Thingy thingy;

    return thingy;
}

// create function
void create()
{
     get();
}

void use()
{
    // Ensure only one thread get to create first before all other
    boost::call_once( &create, flag );

    // get a constructed thingy
    const Thingy & thingy = get(); 

    // use it
    thingy.etc..()          
}

Насколько я понимаю, таким образом все потоки ждут boost :: call_once, кроме одного, который создаст статическую переменную. Он будет создан только один раз и больше никогда не будет вызван. И тогда у вас больше нет блокировки.

person Nikko    schedule 02.06.2010
comment
У меня такое же понимание, выглядит очень хорошо, хотя мне интересно, можно ли как-то обернуть его так, чтобы call_once не был виден и не мог быть забыт. - person Matthieu M.; 02.06.2010
comment
Я знаю, что есть способы сделать это правильно, и, возможно, boost::once это дешевый способ сделать это. Однако это не отвечает на мой вопрос, достаточно ли хороша практика, которую я использовал в прошлом, чтобы я не возвращался к старому коду и не исправлял все это. - person sbi; 08.06.2010
comment
Для меня это нормально, это работает, это было просто решение, чтобы избежать накладных расходов на блокировку - person Nikko; 08.06.2010

Итак, соответствующая часть спецификации - 6.7 / 4:

Реализации разрешается выполнять раннюю инициализацию других локальных объектов со статической продолжительностью хранения при тех же условиях, при которых реализации разрешается статически инициализировать объект со статической продолжительностью хранения в области пространства имен (3.6.2). В противном случае такой объект инициализируется при первом прохождении управления через его объявление; такой объект считается инициализированным после завершения его инициализации.

Предполагая, что вторая часть содержит (object is initialized the first time control passes through its declaration), ваш код можно считать потокобезопасным.

При чтении 3.6.2 выясняется, что разрешенная ранняя инициализация - это преобразование динамической инициализации в статической инициализации. Поскольку статическая инициализация должна происходить перед любой динамической инициализацией и поскольку я не могу придумать никакого способа создать поток, пока вы не дойдете до динамической инициализации < / em>, такая ранняя инициализация также гарантирует, что конструктор будет вызван один раз.

Обновить

Итак, что касается вызова конструктора some_type для the_const_thingy, ваш код верен в соответствии с правилами.

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

person R Samuel Klatchko    schedule 21.06.2010
comment
@ Кристофер: Я так не думаю. Текущий стандарт даже не признает существование потоков, поэтому я не думаю, что эту фразу можно интерпретировать таким образом. В нем конкретно ничего не говорится о нарушении уже инициализированной ссылки. - person sbi; 21.06.2010
comment
Если стандарт не признает существование потоков, вы вообще не можете делать никаких предположений о безопасности ... - person bdonlan; 21.06.2010
comment
@bdonlan: Я писал об этом в своем вопросе. Вы вообще его как следует читали? - person sbi; 22.06.2010

Я не стандартист ...

Но для использования, о котором вы упомянули, почему бы просто не инициализировать их до создания какого-либо потока? Многие проблемы синглтонов возникают из-за того, что люди используют идиоматическую ленивую инициализацию «одного потока», в то время как они могут просто создать экземпляр значения при загрузке библиотеки (как типичный глобальный).

Ленивая мода имеет смысл только в том случае, если вы используете это значение из другого «глобального».

С другой стороны, еще один метод, который я видел, заключался в использовании какой-то координации:

  • 'Singleton' для регистрации своего метода инициализации в объекте 'GlobalInitializer' во время загрузки библиотеки.
  • GlobalInitializer будет вызываться в main перед запуском любого потока

хотя, возможно, я неточно это описываю.

person Matthieu M.    schedule 02.06.2010
comment
Я также согласен, если вам не нужен lazy-init, просто инициализируйте его до запуска любого потока - person Nikko; 02.06.2010
comment
Матье: Это была база кода из нескольких MLoC, которая в основном разрабатывалась без поддержки потоков и ее нужно было перенести в MT. Я перебрал и исправил десятки статических объектов, локальных для функций, описанным мной способом, потому что некоторый код уже был написан таким образом, и я предпочел, чтобы код был согласованным. Казалось, это сработало, но я боюсь, что когда-нибудь в будущем это может сломаться, поэтому я и спрашивал. Инициализация десятков таких локальных статических функций при запуске приложения могла бы сработать (если бы я мог быть уверен, что все были перехвачены), но это было бы нежелательно. (Для большинства запусков даже половина из них не была инициализирована.) - person sbi; 08.06.2010

Вкратце, я думаю, что:

  • Инициализация объекта является потокобезопасной, предполагая, что some_mutex полностью сконструирован при вводе create_const_thingy.

  • Не гарантируется, что инициализация ссылки на объект внутри use_const_thingy будет потокобезопасной; он может (как вы говорите) подвергаться многократной инициализации (что менее проблематично), но он также может быть подвержен разрыву слов, что может привести к неопределенному поведению.

[Я предполагаю, что ссылка C ++ реализована как ссылка на фактический объект с использованием значения указателя, которое теоретически может быть прочитано при частичной записи].

Итак, чтобы попытаться ответить на ваш вопрос:

  1. Достаточно безопасно на практике: очень вероятно, но в конечном итоге зависит от размера указателя, архитектуры процессора и кода, сгенерированного компилятором. Суть здесь, вероятно, заключается в том, является ли запись / чтение размером с указатель атомарной или нет.

  2. Безопасно по правилу: ну, извините, в C ++ 98 таких правил нет (но вы это уже знали).


Обновление: после публикации этого ответа я понял, что он фокусируется только на небольшой, эзотерической части реальной проблемы, и из-за этого решил опубликовать другой ответ вместо редактирования содержимого. Я оставляю содержание «как есть», поскольку оно имеет некоторое отношение к вопросу (а также для того, чтобы смириться, напоминая мне, чтобы я подумал еще немного, прежде чем отвечать).

person rjnilsson    schedule 21.06.2010

Я запрограммировал достаточно межпроцессных сокетов, чтобы мне снились кошмары. Чтобы сделать что-либо поточно-ориентированным на ЦП с ОЗУ DDR, вы должны выровнять структуру данных по строкам кэша и упаковать все ваши глобальные переменные непрерывно в как можно меньше строк кеша.

Проблема с невыровненными межпроцессными данными и слабо упакованными глобальными объектами заключается в том, что они вызывают наложение из-за промахов кеша. В процессорах, использующих DDR RAM, есть (обычно) набор из 64-байтовых строк кэша. Когда вы загружаете строку кэша, DDR RAM автоматически загружает еще несколько строк кэша, но первая строка кэша всегда самая горячая. Что происходит с прерываниями, которые происходят на высоких скоростях, так это то, что страница кеша будет действовать как фильтр нижних частот, как и в аналоговых сигналах, и будет отфильтровывать данные прерывания, что приводит к ПОЛНОСТЬЮ сбивающим с толку ошибкам, если вы не в курсе, что происходит. То же самое и с глобальными переменными, которые не упакованы плотно; если он занимает несколько строк кэша, он выйдет из синхронизации, если вы не сделаете снимок критических межпроцессных переменных и не передадите их в стек и регистры, чтобы обеспечить правильную синхронизацию данных.

Раздел .bss (то есть, где хранятся глобальные переменные, будет инициализирован всеми нулями, но компилятор не будет выравнивать данные за вас по строкам кеша, вам придется сделать это самостоятельно, что также может быть хорошим местом для используйте C ++ Construct in Place. Чтобы изучить математику, лежащую в основе самый быстрый способ выровнять указатели прочтите в этой статье; Я пытаюсь понять, придумал ли я этот трюк. Вот как будет выглядеть код:

inline char* AlignCacheLine (char* buffer) {
  uintptr_t offset = ((~reinterpret_cast<uintptr_t> (buffer)) + 1) & (63);
  return buffer + offset;
}

char SomeTypeInit (char* buffer, int param_1, int param_2, int param_3) {
  SomeType type = SomeType<AlignCacheLine (buffer)> (1, 2, 3);
  return 0xff;
}

const SomeType* create_const_thingy () {
  static char interprocess_socket[sizeof (SomeType) + 63],
              dead_byte = SomeTypeInit (interprocess_socket, 1, 2, 3);
  return reinterpret_cast<SomeType*> (AlignCacheLine (interprocess_socket));
}

По моему опыту, вам придется использовать указатель, а не ссылку.

person Community    schedule 13.06.2018

Просто вызовите функцию перед тем, как начать создавать потоки, тем самым гарантируя ссылку и объект. В качестве альтернативы, не используйте такой по-настоящему ужасный шаблон проектирования. Я имею в виду, зачем вообще статическая ссылка на статический объект? Зачем вообще статические объекты? В этом нет никакой пользы. Синглтоны - ужасная идея.

person Puppy    schedule 21.06.2010
comment
<sigh> Что такого сложного в понимании Меня не интересует, что я могу сделать вместо этого, я действительно хочу знать об этом? Я даже выделил это жирным шрифтом и также указал точную причину, по которой я хочу знать это... - person sbi; 21.06.2010
comment
@sbi: Упс. Я не ожидал, что на открывающем плакате все еще читаются ответы, поскольку этой теме уже две недели. - person Puppy; 21.06.2010
comment
Просто продолжайте и внимательно прочтите вопрос. Там написано, что на плакате объявлено вознаграждение. (Кроме того, тот, кто задает вопрос, в любом случае получает информацию о любом новом ответе.) - person sbi; 21.06.2010

Кажется, это самый простой / самый чистый подход, о котором я могу думать, без необходимости использования всех мьютексных шананиганов:

static My_object My_object_instance()
{
    static My_object  object;
    return object;
}

// Ensures that the instance is created before main starts and creates any threads
// thereby guaranteeing serialization of static instance creation.
__attribute__((constructor))
void construct_my_object()
{
    My_object_instance();
}
person Chappelle    schedule 12.06.2012
comment
Но это C ++ 11, который не был выпущен, когда я писал этот вопрос. - person sbi; 13.06.2012
comment
Тем не менее, если бы я читал эту ветку сегодня, я бы хотел узнать лучшее / текущее решение проблемы. - person Chappelle; 13.06.2012
comment
Однако я специально не просил решения. Я спросил, безопасен ли мой старый код или его нужно исправить. - person sbi; 01.01.2013