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

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

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

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


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

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

9 ответов


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

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

из редакции 2003 года стандарта C++:

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

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

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

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


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

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

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

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 ()), а затем выбрасывать дополнительные функции. The OSAtomicCompareAndSwapPtrBarrier функция, предоставляемая в Mac OS X-большинство операционных систем предоставляют аналогичный примитив-проверяет, является ли pObj и NULL и только фактически устанавливает его в temp это если он есть. Это использует аппаратную поддержку действительно, буквально только выполните обмен после и скажите, произошло ли это.

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


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

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


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

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


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

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

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

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

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


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

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

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

Edit: да, Дерек, ты прав. Моя ошибка. :)


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

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

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


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

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

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

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

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


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

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