Singleton. Облом года.
{
static HANDLE hMutex = INVALID_HANDLE_VALUE;
if ( hMutex == INVALID_HANDLE_VALUE ) hMutex = CreateMutex( NULL, TRUE, NULL );
if ( !pInstance )
{
WaitForSingleObject( hMutex, INFINITY );
if ( !pInstance ) pInstance = new Button::Unpressed();
}
return pInstance;
}
как следует из вышеприведенного кода существует опасность создания более чем одного объекта-мьютекса, причем хандл одного из них оказывается утерянным. Таким образом прибегая к блокировке при помощи объектов ядра мы всего лишь рекурсивно получаем ту же проблему : получение единственного объекта - теперь уже мьютекса. Вот в Java можно сделать целый метод синхронизированым объявив его synchronized. С точки зрения данного уровня абстракции проблема вроде решена. Вот только вопрос : как реализована в программной Java-машине инструкция synchronized под Windows?
Я вот тут седня полдня девелопел Singleton устойчиво работающий в мультипоточной среде. Так вот оказывается его не существует. Даже для Windows. Получается Алекснадреску зря написал целую главу посвященную ему в своей книге. Бросаю вызов : пусть кто нить докажет что приведенный им код надежно работает в мультипоточной среде. Как известно это блокировка с двойной проверкой. Но она не работает.
Без паники!
Не следует делать глобальных категоричных выводов из своего неудачного эксперимента.
Не работает лишь твоя реализация.
Таким образом прибегая к блокировке при помощи объектов ядра мы всего лишь рекурсивно получаем ту же проблему : получение единственного объекта - теперь уже мьютекса.
ОС здесь не при чем, здесь проблема реализации.
Классический код для thread-safe singleton-а вышлядит так:
{
if (!_instance) {
Locker lock(guard);
if (!_instance)
_instance = create();
}
return *_instance;
}
Вся проблема проблема в том, что ты неверно реализовал залочку.
Создание guard в твоей реализации не атомарно. Кстати, Александреску делает акцент на атомарность.
Есть два решения:
1) создавать именованный объект синхронизации;
2) создавать объект синхронизации заранее и единым для класса синглетона, т.е. объявить статиком, но не для метода, а для класса.
Мне нравится второй вариант, а в качестве объекта лучше взять CS, а не Mutex. Бестрее CS работеает, чем Mutex.
{
public:
CriticalSection()
{
InitializeCriticalSection( &cs );
}
~CriticalSection()
{
DeleteCriticalSection( &cs );
}
Lock()
{
EnterCriticalSection( &cs );
}
Unlock()
{
LeaveCriticalSection( &cs );
}
protected:
CRITICAL_SECTION cs;
};
class Button
{
public:
............
class Unpressed
{
public:
Unpressed* Instance();
protected:
static CriticalSection cs;
.........
};
.....
};
CriticalSection Button::Unpressed::cs;
Button::Unpressed* Button::Unpressed::Instance()
{
if ( !pInstance )
{
cs.Lock();
if ( !pInstance ) pInstance = new Button::Unpressed();
cs.Unlock();
}
return pInstance;
}
Проблема вроде решена. Но как ? Если раньше я задавал вопрос как реализован метод synchronized в Java, то теперь его можно заменить на вопрос как реализован код
Кстати интересно посмотреть реализацию
Фактически проблема решена путем выноса процесса создания критической секции в момент выполнения программы до функции main. То есть когда в процессе гарантированно существует один поток и возможность выполнения несколькими потоками данного кода не существует. Но такой код тогда можно назвать только однопоточным. Таким образом, исходная задача : получение единственного объекта в мультипоточной среде не решена. А точнее она решена только относительно факта существования единственного обьекта, созданного в однопоточной среде.
Именованный Mutex гарантированно будет создан только в одном экземпляре, не зависимо от кол-ва потоков, процессов или процессоров.( если, конечно, имя везде одинаковое).
{
static HANDLE hMutex = INVALID_HANDLE_VALUE;
if ( hMutex == INVALID_HANDLE_VALUE )
{
hMutex = CreateMutex( "MyMutex", TRUE, NULL );
}
if ( !pInstance )
{
WaitForSingleObject( hMutex, INFINITY );
if ( !pInstance )
{
pInstance = new Button::Unpressed();
}
ReleaseMutex( hMutex ); // Нужно освобождать
}
return pInstance;
}
А hMutex нужно будет освободить
Фактически проблема решена путем выноса процесса создания критической секции в момент выполнения программы до функции main. То есть когда в процессе гарантированно существует один поток и возможность выполнения несколькими потоками данного кода не существует. Но такой код тогда можно назвать только однопоточным. Таким образом, исходная задача : получение единственного объекта в мультипоточной среде не решена. А точнее она решена только относительно факта существования единственного обьекта, созданного в однопоточной среде.
Без однопоточного кода, а точнее без атомарной операции, не обойтись, иначе никто не может гарантировать целостность транзакции. Это верное не только для компьютеров, но IMHO, для любых систем. На сколько мне известно, это одна из проблем на пути создания квантовых компьютеров.
Так что придется смириться... :)
Создание именованного объекта синхронизации - один из вариантов реализации гарантированного однопоточного выполнения, только в этом случае это задача ОС. Поэтому создание именованного мьютекса более трудоемкий процесс, чем создание и дальнейшее использование CS.
Кстати интересно посмотреть реализацию
Да там всё элементарно. В конструкторе Locker залочиваем guard, в деструкторе - разлочиваем.
{
public:
Locker(CriticalSection& cs) :_cs(cs) {
cs.Lock();
}
~Locker() {
cs.Unlock();
}
private:
CriticalSection& _cs;
};
А также вопрос где можно скачать библиотеку Loki из книги Александреску. Приведеная в книге ссылка не работает.
Думаю, здесь google поможет.
Но применять на практике не советую... Лучше уж boost.
#include "CriticalSection.h"
template<class T> class Singleton
{
public:
static T* Instance()
{
if ( !_Instance )
{
_CriticalSection.Lock();
if ( !_Instance ) _Instance = new T;
_CriticalSection.Unlock();
}
return _Instance;
}
protected:
static T* _Instance;
static CriticalSection _CriticalSection;
};
template<class T> T* Singleton<T>::_Instance = 0;
template<class T> CriticalSection Singleton<T>::_CriticalSection;