Вопрос

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

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

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


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

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

Решение

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

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

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

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

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

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

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

Другие советы

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

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

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.Thread для этой цели.Прочтите раздел «Одноразовая инициализация» для получения дополнительной информации.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top