Как я могу создать потокобезопасный шаблон синглтона в Windows? - PullRequest
16 голосов
/ 03 октября 2008

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

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 - Примечание: для будущих читателей, если вы не используете этот компилятор (или 2005) - не используйте принятый ответ !!

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

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

Ответы [ 9 ]

13 голосов
/ 03 октября 2008

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

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

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

// 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
        }
};

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

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

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

11 голосов
/ 03 октября 2008

Если вы используете 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, чтобы сделать это возможным.

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

4 голосов
/ 21 апреля 2012

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

1 голос
/ 03 октября 2008

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

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

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

1 голос
/ 03 октября 2008

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

0 голосов
/ 21 июня 2012

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

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

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

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

А http://msdn.microsoft.com/en-us/library/windows/desktop/ms682530(v=vs.85).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» Скотт Мейер назвал «однопоточной частью запуска» в «Effective C ++ 3rd», пункт 4.

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

0 голосов
/ 21 октября 2008

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

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

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

0 голосов
/ 03 октября 2008

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

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

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

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

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

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

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

0 голосов
/ 03 октября 2008

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

  • который вы никогда не должны использовать
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...