Потокобезопасная инициализация локально-функциональных статических объектов const - PullRequest
19 голосов
/ 02 июня 2010

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

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

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. безопасно по правилам? (Я знаю, что текущий стандарт даже не знает, что такое «параллелизм», но как насчет того, чтобы растоптать уже инициализированную ссылку?

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


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

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


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

Ответы [ 8 ]

14 голосов
/ 22 июня 2010

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

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

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

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

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

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

5 голосов
/ 02 июня 2010

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

Я видел (и использовал) нечто подобное для защиты статической инициализации, используя 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, кроме одного, который создаст статическую переменную. Он будет создан только один раз и больше никогда не будет вызван. И тогда у тебя больше нет блокировки.

3 голосов
/ 21 июня 2010

Итак, соответствующая часть спецификации равна 6,7 / 4:

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

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

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

Обновление

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

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

0 голосов
/ 13 июня 2018

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

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

Раздел .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));
}

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

0 голосов
/ 13 июня 2012

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

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();
}
0 голосов
/ 21 июня 2010

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

0 голосов
/ 21 июня 2010

Короче говоря, я думаю, что:

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

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

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

Итак, попробуйтеи ответьте на свой вопрос:

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

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


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

0 голосов
/ 02 июня 2010

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...