Поточно-безопасное ленивое построение синглтона в C ++

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

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

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


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

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

person pauldoo    schedule 09.08.2008    source источник


Ответы (9)


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

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

Из версии стандарта C ++ 2003 года:

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

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

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

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

person Derek Park    schedule 09.08.2008

К сожалению, в ответе Мэтта есть так называемая блокировка с двойной проверкой, которая не поддерживается моделью памяти 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. Это позволяет вам настроить функцию, которая запускается только один раз - в основном, выполняя все блокировки / барьеры / и т. Д. обман для вас - независимо от того, сколько раз он вызывается или в скольких потоках он вызывается.

person Chris Hanson    schedule 09.08.2008

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

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 по этой теме.

person Frerich Raabe    schedule 19.05.2010
comment
Хороший! Это будет намного лучше, чем наше текущее решение. Когда, наконец, будет завершен C ++ 0x (или C ++ 1x)? ..? - person pauldoo; 20.05.2010
comment
VS2015 представляет поточно-ориентированную поддержку этого шаблона инициализации. - person Chris Betti; 22.11.2015

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

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

person Chris Jester-Young    schedule 10.08.2008
comment
Просто мое мнение, но я считаю, что с Boost нужно быть осторожнее. Я не уверен, что это потокобезопасный, хотя у него много подпроектов, связанных с потоками. (Это после проведения двух аудитов с разницей в несколько лет и просмотра отчетов об ошибках, закрытых, поскольку они не могут быть исправлены). - person jww; 09.01.2014

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

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

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

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

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

person 0124816    schedule 16.09.2008
comment
В этом вы совершенно правы. Но лучше, если вы выделите For VC ++ жирным шрифтом, это не та фраза. blogs.msdn.com/oldnewthing/archive/2004/03/ 08 / 85901.aspx - person Varuna; 18.12.2009

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

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

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

Изменить: Ага, Дерек, ты прав. Виноват. :)

person OJ.    schedule 10.08.2008

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

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

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

person Derek Park    schedule 10.08.2008

  1. читал на модели со слабой памятью. Он может взламывать дважды проверенные блокировки и спин-блокировки. Intel - сильная модель памяти (пока), поэтому на Intel проще

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

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

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

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

person n-alexander    schedule 09.11.2009

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

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

person Mat Noguchi    schedule 20.08.2008