Как я могу создать потокобезопасный одноэлементный шаблон в Windows?

Я читал здесь о потокобезопасных одноэлементных шаблонах:

http://en.wikipedia.org/wiki/Singleton_pattern#C.2B.2B_.28using_pthreads.29

И внизу написано, что единственный безопасный способ - использовать pthread_once, который недоступен в Windows.

Это единственный способ гарантировать потокобезопасную инициализацию?

Я прочитал эту ветку на SO:

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

И, похоже, намекает на атомарную функцию подкачки и сравнения на уровне ОС, которая, как я предполагаю, в Windows:

http://msdn.microsoft.com/en-us/library/ms683568.aspx

Могу ли я сделать то, что я хочу?

Изменить: я бы хотел, чтобы инициализация была отложена, и чтобы был только один экземпляр класса.

Кто-то на другом сайте упомянул об использовании глобального внутри пространства имен (и он описал синглтон как анти-шаблон) - как он может быть «анти-шаблоном»?

Принятый ответ:
Я принял Ответ Джоша, поскольку я использую Visual Studio 2008 - NB: для будущих читателей, если вы не используете этот компилятор (или 2005) - не используйте принятый ответ !!

Изменить: Код работает нормально, за исключением оператора return - я получаю сообщение об ошибке: ошибка C2440: «return»: невозможно преобразовать из «volatile Singleton *» в «Singleton *». Следует ли изменить возвращаемое значение на изменчивый синглтон *?

Изменить: Очевидно, const_cast ‹> удалит квалификатор volatile. Еще раз спасибо Джошу.


person Mark Ingram    schedule 02.10.2008    source источник
comment
Инициализируйте синглтон, прежде чем создавать какие-либо потоки.   -  person Martin York    schedule 03.10.2008
comment
Сингелтон - это антипаттерн, потому что в большинстве случаев он используется неправильно. т.е. как замена глобального варибала.   -  person Martin York    schedule 03.10.2008
comment
Разве глобальные переменные не являются отвратительным откатом от C?   -  person Mark Ingram    schedule 03.10.2008
comment
Как инициализируется CRITICAL_SECTION 'cs' в ответе Джоша?   -  person Matthew Murdoch    schedule 04.10.2008
comment
Отличное обсуждение того, как реализовать синглтон, а также безопасность потоков в C ++, можно найти в этой статье: aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf   -  person    schedule 30.10.2009


Ответы (9)


Если вы используете Visual C ++ 2005/2008, вы можете использовать шаблон блокировки с двойной проверкой, поскольку "изменчивые переменные действуют как ограждения ". Это наиболее эффективный способ реализовать синглтон с отложенной инициализацией.

Из журнала MSDN Magazine:

Singleton* GetSingleton()
{
    volatile static Singleton* pSingleton = 0;

    if (pSingleton == NULL)
    {
        EnterCriticalSection(&cs);

        if (pSingleton == NULL)
        {
            try
            {
                pSingleton = new Singleton();
            }
            catch (...)
            {
                // Something went wrong.
            }
        }

        LeaveCriticalSection(&cs);
    }

    return const_cast<Singleton*>(pSingleton);
}

Когда вам понадобится доступ к синглтону, просто вызовите GetSingleton (). При первом вызове статический указатель будет инициализирован. После инициализации проверка NULL предотвратит блокировку только при чтении указателя.

НЕ используйте это в любом компиляторе, так как он не переносится. Стандарт не дает никаких гарантий относительно того, как это будет работать. Visual C ++ 2005 явно добавляет семантику volatile, чтобы это стало возможным.

Вам нужно будет объявить и инициализировать КРИТИЧЕСКИЙ РАЗДЕЛ в другом месте кода. Но такая инициализация стоит недорого, поэтому отложенная инициализация обычно не важна.

person Eclipse    schedule 02.10.2008
comment
Это не работает: прочтите эту статью erdani.org/publications/DDJ_Jul_Aug_2004_revised.pdf - person Martin York; 03.10.2008
comment
Этот код не безопасен для исключений: LeaveCriticalSection () не будет вызываться, если исключения начнут летать. - person Martin York; 03.10.2008
comment
Кроме того, статья, которую вы предоставили, относится к 2004 году, а статья, предоставленная Джошем, относится к 2007 году. - person Mark Ingram; 03.10.2008
comment
Единственные исключения, которые будут летать, будут перехвачены блоком try / catch, но вы правы, что перенос Enter / Leave CriticalSection должен выполняться классом-оболочкой RAII. - person Eclipse; 03.10.2008
comment
Мартин: Visual C ++ 2005 изменил семантику volatile специально для решения проблем, поднятых в статье, на которую вы ссылались. Если вы попробуете это в 6.0, вы в конечном итоге столкнетесь с проблемами, когда ваш процессор переупорядочивает инструкции или у вас многоядерная система. - person Eclipse; 03.10.2008
comment
Джош прав. Важно отметить, что использование «volatile» здесь специфично для Visual C ++ и не гарантирует потокобезопасность, если код скомпилирован с помощью другого компилятора Windows (Intel, gcc и т. Д.) Или перенесен на другую ОС. - person Matthew Murdoch; 03.10.2008
comment
Ваш код работает нормально, за исключением оператора return - я получаю сообщение об ошибке: ошибка C2440: «return»: невозможно преобразовать из «volatile Singleton » в «Singleton *». Должен ли я изменить возвращаемое значение на изменчивый синглтон? - person Mark Ingram; 03.10.2008
comment
Вы будете в безопасности с const_cast ‹Singleton *› (pSingleton), поскольку изменчивая семантика нужна только внутри критической секции. - person Eclipse; 03.10.2008
comment
const_cast ‹› удаляет изменчивый квалификатор? Сумасшедшие вещи. Хотя работал нормально. Спасибо! - person Mark Ingram; 03.10.2008
comment
Как инициализируется CRITICAL_SECTION 'cs'? - person Matthew Murdoch; 04.10.2008
comment
Маленькая гнида: вы присваиваете 0, но проверяете против NULL. Либо используйте 0 в тесте, либо используйте! PSingleton - person camh; 04.10.2008
comment
Как я могу гарантировать, что CRITICAL_SECTION инициализируется до вызова GetSingleton? - person deft_code; 11.11.2009

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

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

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

// A critical section guard - create on the stack to provide 
// automatic locking/unlocking even in the face of uncaught exceptions
class Guard {
    private:
        LPCRITICAL_SECTION CriticalSection;

    public:
        Guard(LPCRITICAL_SECTION CS) : CriticalSection(CS) {
            EnterCriticalSection(CriticalSection);
        }

        ~Guard() {
            LeaveCriticalSection(CriticalSection);
        }
};

// A thread-safe singleton
class Singleton {
    private:
        static Singleton* Instance;
        static CRITICAL_SECTION InitLock;
        CRITICIAL_SECTION InstanceLock;

        Singleton() {
            // Time consuming initialization here ...

            InitializeCriticalSection(&InstanceLock);
        }

        ~Singleton() {
            DeleteCriticalSection(&InstanceLock);
        }

    public:
        // Not thread-safe - to be called from the main application thread
        static void Create() {
            InitializeCriticalSection(&InitLock);
            Instance = NULL;
        }

        // Not thread-safe - to be called from the main application thread
        static void Destroy() {
            delete Instance;
            DeleteCriticalSection(&InitLock);
        }

        // Thread-safe lazy initializer
        static Singleton* GetInstance() {
            Guard(&InitLock);

            if (Instance == NULL) {
                Instance = new Singleton;
            }

            return Instance;
        }

        // Thread-safe operation
        void doThreadSafeOperation() {
            Guard(&InstanceLock);

            // Perform thread-safe operation
        }
};

Однако есть веские причины вообще избегать использования синглтонов (и почему их иногда называют анти-шаблоном):

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

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

person Community    schedule 02.10.2008

Хотя мне нравится принятое решение, я только что нашел еще одно многообещающее решение и подумал, что должен поделиться им здесь: Одноразовая инициализация (Windows)

person TripShock    schedule 20.04.2012

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

person Henk    schedule 02.10.2008

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

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

В Интернете есть множество примеров для реализации этих шаблонов на C ++. Вот пример проекта кода

person JaredPar    schedule 02.10.2008
comment
Первый (можно создать только один класс) - person Mark Ingram; 03.10.2008

Ниже объясняется, как это сделать на C #, но та же концепция применима к любому языку программирования, который поддерживает шаблон singleton.

http://www.yoda.arachsys.com/csharp/singleton.html

Вам нужно решить, нужна вам ленивая инициализация или нет. Ленивая инициализация означает, что объект, содержащийся внутри синглтона, создается при первом его вызове ex:

MySingleton::getInstance()->doWork();

если этот вызов не будет выполнен позже, существует опасность состояния гонки между потоками, как описано в статье. Однако если вы положите

MySingleton::getInstance()->initSingleton();

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

person Eric    schedule 02.10.2008
comment
Я не думаю, что такая же концепция применима к любому языку программирования, для C # это просто, язык гарантирует, что статический ctor будет вызываться только один раз и является потокобезопасным. Для C ++ это сложно. - person zhaorufei; 21.06.2012

Если вы ищете более портативное и простое решение, вы можете обратиться к boost.

boost :: call_once можно использовать для потоковой безопасности инициализация.

Он довольно прост в использовании и станет частью следующего стандарта C ++ 0x.

person mmocny    schedule 21.10.2008

Вопрос не требует, чтобы синглтон был ленивым или нет. Поскольку многие ответы предполагают это, я предполагаю, что для первой фразы обсудите:

Учитывая тот факт, что язык сам по себе не поддерживает потоки, и плюс техника оптимизации, написание переносимого надежного синглтона на С ++ очень сложно (если не невозможно), см. "C ++ и опасности двойной проверки блокировки" Скотта Мейерса и Андрея Александреску.

Я видел, как многие из ответов прибегают к синхронизации объекта на платформе Windows с помощью CriticalSection, но CriticalSection является потокобезопасным только тогда, когда все потоки выполняются на одном процессоре, сегодня это, вероятно, неверно.

Цитата из MSDN: «Потоки одного процесса могут использовать объект критической секции для синхронизации взаимного исключения».

И http://msdn.microsoft.com/en-us/library/windows/desktop/ms682530%28v=vs.85%29.aspx

проясните это дальше:

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

Теперь, если "ленивое построение" не является обязательным, следующее решение является кросс-модульным, поточно-ориентированным и даже переносимым:

struct X { };

X * get_X_Instance()
{
    static X x;
    return &x;
}
extern int X_singleton_helper = (get_X_instance(), 1);

Это кросс-модульно-безопасно, потому что мы используем статический объект с локальной областью видимости вместо глобального объекта с областью видимости файла / пространства имен.

Это потокобезопасно, потому что: X_singleton_helper должно быть присвоено правильное значение перед вводом main или DllMain Это не ленивое построение также из-за этого факта), в этом выражении запятая является оператором, а не пунктуацией.

Явно используйте здесь extern, чтобы компилятор не оптимизировал его (опасения по поводу статьи Скотта Мейерса, большой враг - оптимизатор.), А также заставьте замолчать инструмент статического анализа, такой как pc-lint. «Перед main / DllMain» Скотт Мейер назвал «однопоточную часть запуска» в пункте 4 «Эффективный C ++ 3-й».

Однако я не очень уверен, разрешено ли компилятору оптимизировать вызов get_X_instance () out в соответствии со стандартом языка, прокомментируйте.

person zhaorufei    schedule 21.06.2012
comment
Я должен восхищаться тем, насколько я глуп, говоря о путанице процессора и процесса. Critical, конечно, можно использовать в многопроцессорном / многоядерном оборудовании, если только в рамках одного процесса. Вместо того, чтобы удалить неправильный раздел выше, я оставлю его здесь и использую этот комментарий, чтобы напомнить себе о своей вине. - person zhaorufei; 27.04.2014

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

  • который вы никогда не должны использовать
person 1800 INFORMATION    schedule 02.10.2008