Потокобезопасная ленивая конструкция синглтона в C ++

Есть ли способ реализовать одноэлементный объект в C ++, который:

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

(Я недостаточно хорошо знаю мой C ++, но это тот случай, когда целочисленные и постоянные статические переменные инициализируются до выполнения любого кода (т. Е. Даже до того, как выполняются статические конструкторы - их значения уже могут быть «инициализированы» в программе) image)? Если так - возможно, это можно использовать для реализации мьютекса синглтона - который, в свою очередь, можно использовать для защиты создания реального синглтона ..)


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

  1. Использовать статическую инициализацию (в отличие от динамической инициализации) статической переменной POD и реализовывать свой собственный мьютекс с помощью встроенных атомарных инструкций. Это был тип решения, на которое я намекал в своем вопросе, и я думаю, что я уже знал.
  2. Используйте некоторые другие библиотечные функции, такие как pthread_once или boost :: call_once . Об этом я точно не знал - и очень благодарен за ответы.
9 ОТВЕТОВ
РЕШЕНИЕ

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

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

Начиная с версии 2003 года стандарта C ++:

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

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

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

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

12
10.08.2008 00:06:19

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

Кроме того, как признает Мэтт, он использует intв качестве блокировки, а не примитив ОС. Не делай этого. Правильные блокировки требуют использования инструкций барьера памяти, потенциально очистки строки кэша и т. Д .; используйте примитивы вашей операционной системы для блокировки. Это особенно важно, потому что используемые примитивы могут меняться между отдельными линиями ЦП, на которых работает ваша операционная система; что работает на CPU Foo, может не работать на CPU 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
20.08.2008 16:41:35

Вы не можете сделать это без каких-либо статических переменных, однако, если вы хотите терпеть одну, вы можете использовать Boost.Thread для этой цели. Прочтите раздел «Одноразовая инициализация» для получения дополнительной информации.

Затем в функции одноэлементного метода доступа используйте boost::call_onceдля создания объекта и возврата его.

8
10.08.2008 04:13:08
Просто мое мнение, но я думаю, что вы должны быть осторожны с Boost. Я не уверен, что это потокобезопасно, хотя в нем есть много подпроектов, связанных с многопоточностью. (Это после проведения двух проверок с интервалом в несколько лет и просмотра отчетов об ошибках, закрытых как «не будет исправлено»).
jww 9.01.2014 09:30:58

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

  • Если вы хотите ленивую реализацию экземпляра синглтона при использовании указателя на динамически размещенный экземпляр, вам нужно убедиться, что вы его очистили в нужной точке.
  • Вы могли бы использовать решение Мэтта, но вам нужно было бы использовать надлежащий мьютекс / критическую секцию для блокировки и проверки «pObj == NULL» как до, так и после блокировки. Конечно, pObj также должен быть статичным ;). В этом случае мьютекс будет неоправданно тяжелым, вам лучше пойти с критическим разделом.

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

Редактировать: Да, Дерек, ты прав. Виноват. :)

1
10.08.2008 05:18:14

Вы могли бы использовать решение Мэтта, но вам нужно было бы использовать надлежащий мьютекс / критическую секцию для блокировки и проверки «pObj == NULL» как до, так и после блокировки. Конечно, pObj также должен быть статичным;). В этом случае мьютекс будет неоправданно тяжелым, вам лучше пойти с критическим разделом.

Ой, это не работает. Как отметил Крис, это двойная проверка блокировки, которая не гарантируется в текущем стандарте C ++. См .: C ++ и опасности двойной проверки блокировки

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

1
10.08.2008 05:27:50

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

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

0
3.07.2012 14:56:23

Для gcc это довольно просто:

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

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

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

Кроме того, возможно, в стороне: лучше всегда избегать статических типов не POD. (С указателями на POD все в порядке.) Причин для этого много: как вы упоминаете, порядок инициализации не определен, равно как и порядок вызова деструкторов. Из-за этого программы заканчивают сбоем, когда они пытаются выйти; часто это не имеет большого значения, но иногда требуется приставка, когда профилировщик, который вы пытаетесь использовать, требует чистого выхода.

6
31.08.2010 14:12:31
Вы совершенно правы в этом. Но лучше, если вы выделите фразу «Для VC ++ это не символ». blogs.msdn.com/oldnewthing/archive/2004/03/08/85901.aspx
Varuna 18.12.2009 00:49:53
  1. читать на слабой модели памяти. Это может сломать дважды проверенные замки и спин-блокировки. Intel сильная модель памяти (пока), поэтому на Intel это проще

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

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

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

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

1
9.11.2009 17:32:29

Вот очень простой ленивый синглтон-геттер:

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
19.05.2010 16:20:12
Приятно! Это будет намного лучше, чем наше текущее решение. Когда C ++ 0x (или это должен быть C ++ 1x) наконец будет закончен?
pauldoo 20.05.2010 10:20:30
VS2015 представляет поточно-ориентированную поддержку для этого шаблона инициализации.
Chris Betti 22.11.2015 15:00:39