Подтвердить что ты не робот

Потоковое безопасное ленивое построение одноэлементного кода в С++

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

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

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


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

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

Ответ 1

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

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

В редакции 2003 года стандарта С++:

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Также см. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html в этом разделе.

Ответ 4

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

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

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

  • такие объекты трудно правильно уничтожить

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

Ответ 9

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

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

Ответ 10

@Mat

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

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

MSN

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