Ориентированная на многопотоковое исполнение ленивая конструкция одиночного элемента в C++

Как будто вы пытаетесь получить доступ к объекту, который является null. Рассмотрим ниже пример:

TypeA objA;

. В это время вы только что объявили этот объект, но не инициализировали или не инициализировали. И всякий раз, когда вы пытаетесь получить доступ к каким-либо свойствам или методам в нем, он будет генерировать NullPointerException, что имеет смысл.

См. Также этот пример:

String a = null;
System.out.println(a.toString()); // NullPointerException will be thrown
36
задан Upendra Chaudhari 20 October 2011 в 07:35
поделиться

9 ответов

В основном Вы просите синхронизируемое создание одиночного элемента, не используя синхронизации (ранее созданные переменные). В целом, нет, это не возможно. Вам нужно что-то доступное для синхронизации.

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

От пересмотра 2003 года стандарта C++:

Объекты со статической продолжительностью хранения (3.7.1) должны быть инициализированы нулем (8.5), прежде чем любая другая инициализация произойдет. Нулевую инициализацию и инициализацию с константным выражением коллективно называют статической инициализацией; вся другая инициализация является динамической инициализацией. Объекты типов POD (3.9) со статической продолжительностью хранения, инициализированной с константными выражениями (5.19), должны быть инициализированы, прежде чем любая динамическая инициализация происходит. Объекты со статической продолжительностью хранения, определенной в объеме пространства имен в той же единице перевода и динамично инициализированной, должны быть инициализированы в порядке, в котором их определение появляется в единице перевода.

, Если Вы знаете , что Вы будете использовать этот одиночный элемент во время инициализации других статических объектов, я думаю, что Вы найдете, что синхронизация является надуманным вопросом. Насколько я знаю все главные компиляторы инициализируют статические объекты в единственном потоке, таким образом, потокобезопасность во время статической инициализации. Можно объявить, что одноэлементный указатель является ПУСТЫМ, и затем проверяет, чтобы видеть, был ли он инициализирован перед использованием его.

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

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

12
ответ дан Derek Park 27 November 2019 в 06:02
поделиться

К сожалению, функции ответа Matt, что звонило перепроверяемая блокировка , который не поддерживается моделью памяти C/C++. (Это поддерживается Java 1.5 и позже —, и я думаю.NET — модель памяти.) Это означает, что между временем, когда эти pObj == NULL проверка происходит и когда блокировка (взаимное исключение) получена, pObj, возможно, был уже присвоен на другом потоке. Переключение потока происходит каждый раз, когда ОС хочет его к, не между "строками" программы (которые не имеют никакой посткомпиляции значения на большинстве языков).

, Кроме того, как Matt подтверждает, он использует int в качестве блокировки, а не примитивной ОС. Не делайте этого. Надлежащие блокировки требуют использования инструкций по барьеру памяти, сбросов потенциально строки кэша, и так далее; используйте примитивы своей операционной системы для блокировки. Это особенно важно, потому что используемые примитивы могут измениться между отдельными строками ЦП, на которых работает Ваша операционная система; какие работы над Нечто ЦП не могли бы работать над ЦП Foo2. Большинство операционных систем или исходно поддерживает потоки POSIX (pthreads) или предлагает им как обертка для пакета поточной обработки ОС, таким образом, часто лучше проиллюстрировать примеры с помощью них.

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

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

Это только работает, если безопасно создать несколько экземпляров Вашего одиночного элемента (один на поток, который, оказывается, вызывает GetSingleton () одновременно), и затем выбросьте отдельно оплачиваемые предметы. Эти OSAtomicCompareAndSwapPtrBarrier функция, обеспеченная на Mac OS X — большинство операционных систем, обеспечивает подобный примитивный — проверки, является ли pObj NULL и только на самом деле устанавливает его на temp к нему, если это. Это использует поддержку оборудования для действительно, буквально только выполните подкачку однажды и скажите, произошло ли это.

Другое средство, чтобы усилить, если Ваша ОС предлагает его, это промежуточно, эти два экстремальных значения pthread_once. Это позволяет Вам настроить функцию, которую это выполнило только однажды - в основном путем выполнения всей блокировки/барьера/и т.д. обман для Вас - неважно, сколько раз это вызывается или на том, сколько потоков это вызывается.

14
ответ дан Derek Park 27 November 2019 в 06:02
поделиться

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

Тогда в Вашей одноэлементной функции средства доступа, используйте boost::call_once, чтобы создать объект и возвратить его.

8
ответ дан Chris Jester-Young 27 November 2019 в 06:02
поделиться

Для gcc это довольно легко:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC удостоверится, что инициализация является атомарной. Для VC ++, дело обстоит не так .:-(

Одной главной проблемой с этим механизмом является отсутствие тестируемости: если необходимо сбросить LazyType к новому между тестами или хотеть изменить LazyType* на MockLazyType*, Вы не будете в состоянии. Учитывая это, обычно лучше использовать статическое взаимное исключение + статический указатель.

кроме того, возможно в стороне: Лучше всегда избегать статических типов не-POD. (Указатели на ПЕРЕХОДНЫЕ ПРИСТАВКИ в порядке.) Причины этого - многие: как Вы упоминаете, порядок инициализации не определяется - ни один не порядок, в котором называют деструкторы все же. Из-за этого программы закончат тем, что отказали, когда они попытаются выйти; часто не грандиозное предприятие, но иногда showstopper, когда профилировщик Вы пытаетесь использовать, требует чистого выхода.

6
ответ дан bguiz 27 November 2019 в 06:02
поделиться

В то время как на этот вопрос уже ответили, я думаю, что существуют некоторые другие точки для упоминания:

  • , Если Вы хотите ленивое инстанцирование одиночного элемента при использовании указателя на динамично выделенный экземпляр, необходимо будет удостовериться, что Вы очищаете его в правой точке.
  • Вы могли использовать решение Matt, но необходимо будет использовать надлежащее взаимное исключение / критический раздел для блокировки, и путем проверки "pObj == ПУСТОЙ УКАЗАТЕЛЬ" и прежде и после блокировки. Конечно, pObj должен был бы также быть статичен ;). Взаимное исключение было бы излишне тяжело в этом случае, Вы будете лучше идти с критическим разделом.

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

Редактирование: Да Derek, Вы правы. Мое плохое.:)

1
ответ дан OJ. 27 November 2019 в 06:02
поделиться

Вы могли использовать решение Matt, но необходимо будет использовать надлежащее взаимное исключение / критический раздел для блокировки, и путем проверки "pObj == ПУСТОЙ УКАЗАТЕЛЬ" и прежде и после блокировки. Конечно, pObj должен был бы также быть статичным;). Взаимное исключение было бы излишне тяжело в этом случае, Вы будете лучше идти с критическим разделом.

OJ, который не работает. Как Chris указал, это - блокировка перепроверки, которая, как гарантируют, не будет работать в текущем стандарте C++. См.: C++ и Опасности Перепроверяемой Блокировки

Редактирование: Без проблем, OJ. Действительно хорошо на языках, где это действительно работает. Я ожидаю, что это будет работать в C++ 0x (хотя я не уверен), потому что это - такая удобная идиома.

1
ответ дан Derek Park 27 November 2019 в 06:02
поделиться

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

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

0
ответ дан Tim Cooper 27 November 2019 в 06:02
поделиться
  1. чтение на слабой модели памяти. Он может взламывать дважды проверенные блокировки и спин-блокировки. Intel - сильная модель памяти (пока), поэтому на Intel проще

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

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

  4. , такие объекты трудно уничтожить должным образом

В целом синглтоны трудно сделать правильно и трудно отладить. Их лучше вообще избегать.

1
ответ дан 27 November 2019 в 06:02
поделиться

Вот очень простой лениво сконструированный одноэлементный геттер:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

Это ленивый метод, и следующий стандарт C ++ (C ++ 0x) требует, чтобы он был потокобезопасным. Фактически, я считаю, что по крайней мере g ++ реализует это потокобезопасным способом. Так что, если это ваш целевой компилятор или , если вы используете компилятор, который также реализует это потокобезопасным образом (может быть, это делают более новые компиляторы Visual Studio? Я не знаю), то это может быть все, что вам нужно.

См. Также http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html по этой теме.

11
ответ дан 27 November 2019 в 06:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: