Оценка 70 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
| Идея Реализация Пример использования Проблемы | ![]() |
Я один, но это не значит, что я одинок... Виктор Цой
Синглтоном (от singleton, одиночка) называется объект, который в любой момент работы системы существует не более чем в одном экземпляре. Часто вводится также дополнительное требование: после своего создания синглтон должен существовать ровно в одном экземпляре, то есть он не должен уничтожаться до окончания работы системы. Обычно такие объекты используются для упорядоченного доступа к каким-то глобальным ресурсам - например к лог-файлу, сетевому соединению, принтеру, пользователю. :)
В зависимости от того, насколько глобален контролируемый синглтоном ресурс и того, что подразумевается под «системой», меняются пределы уникальности синглтона. Синглтон может быть уникален в рамках:
В принципе, все три варианта несложно реализуются стандартными средствами, причём как с использованием COM, так и без.
| ПРИМЕЧАНИЕ Насколько «несложно», сильно зависит от того, что вам надо получить, и что можно использовать. Хотя концептуальных трудностей и нет, всегда могут возникнуть практические. |
Но вот с COM-синглтонами, уникальными в рамках компьютера есть небольшой нюанс: обычно для их реализации предлагается только один подход – оформить COM-сервер в виде exe-файла. В частности, стандартный ATL-синглтон в dll будет уникален только в рамках процесса.
|
Глобальные в пределах машины синглтоны легко получить с помощью СОМ+. Нужно создать обыкновенный синглтон и зарегистрировать его в COM+-приложении. – прим.ред. |
Статья посвящена красивому способу обхода этого ограничения.
Идея заключается в реализации собственной фабрики класса, работающей по следующему алгоритму:
Но это идея «в чистом виде», использование ATL внесёт некоторые коррективы.
В соответствии с идеей, функциональность фабрики класса разбивается на две части:
А, с учётом того, что, если фабрика класса уже зарегистрирована в другом процессе, роль «ядра» исполняет указатель на интерфейс этой фабрики, вырисовывается архитектура:
При этом писать «ядро» самостоятельно совсем не обязательно, можно воспользоваться стандартной ATL-фабрикой класса для синглтонов. Но, если она вас почему-то не устраивает (одна возможная причина описана ниже в разделе «Проблемы»), всегда можно написать свою. Ниже приведен код обертки.
// Первый параметр шаблона – создаваемый класс. От него нам нужен
// только CLSID, для регистрации.
// Второй параметр шаблона – класс, статический метод CreateInstance которого
// умеет создавать «ядро». Звучит страшно, но для ATL вполне стандартно.
template <class T, class RealCFCreator>
class CComClassFactoryDllSingleton :
public IClassFactory,
public CComObjectRootEx<CComGlobalsThreadModel>
{
public:
BEGIN_COM_MAP(CComClassFactoryDllSingleton)
COM_INTERFACE_ENTRY(IClassFactory)
END_COM_MAP()
HRESULT FinalConstruct()
{
m_dwRegister = 0;
return S_OK;
}
HRESULT FinalRelease()
{
if (m_dwRegister != 0)
{
// Надо разрегистрировать фабрику класса
CoRevokeClassObject(m_dwRegister);
}
return S_OK;
}
//// Реализация интерфейса IClassFactory//
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
{
if (ppvObj == 0)
{
return E_POINTER;
}
if (pUnkOuter != NULL)
{
// Синглтоны не поддерживают агрегациюreturn CLASS_E_NOAGGREGATION;
}
// Создаём/получаем фабрику класса
HRESULT hr = GetOrRegisterCF();
if (hr == S_OK)
{
// Пытаемся её использовать
hr = m_pRealClassFactory->CreateInstance(pUnkOuter, riid, ppvObj);
}
return hr;
}
STDMETHOD(LockServer)(BOOL fLock)
{
// Возможно, что до вызова LockServer не было ни одного// вызова CreateInstance, для начала мы должны получить фабрику.
HRESULT hr = GetOrRegisterCF();
if (FAILED(hr))
{
// Не вышлоreturn hr;
}
// Данный вызов идёт либо через "нас" либо, через фабрику в// удалённом процессе.
hr = m_pRealClassFactory->LockServer(fLock);
if (FAILED(hr))
{
// Не вышлоreturn hr;
}
// Чужой модуль – хорошо, но о своём тоже забывать не следуетif (fLock)
{
//_Module.Lock(); // для ATL 3
_pAtlModule->Lock(); // для ATL 7
}
else
{
//_Module.Unlock(); // для ATL 3
_pAtlModule->Unlock(); // для ATL 7
}
return S_OK;
}
private:
// Создаёт и регистрирует новую фабрику класса,// либо получает уже зарегистрированную фабрику.// Результат сохраняется в m_pRealClassFactory.
HRESULT GetOrRegisterCF()
{
if (m_pRealClassFactory != 0)
{
// фабрика уже создана/получена, второй раз не требуетсяreturn S_OK;
}
HRESULT hr = S_OK;
HANDLE hMutex = 0;
__try
{
// Синхронизируем создание фабрики между процессами
hMutex = CreateMutex(0, FALSE, _T("DllSingletonMutex"));
WaitForSingleObject(hMutex, INFINITE);
CLSID clsid = T::GetObjectCLSID();
// Попытаемся получить уже зарегистрированную фабрику класса.
hr = CoGetClassObject(
clsid,
CLSCTX_LOCAL_SERVER,
0,
IID_IClassFactory,
(void**) &m_pRealClassFactory);
if (FAILED(hr))
{
// Фабрика класса ещё не зарегистрирована. Мы - первый процесс// и должны создать и зарегистрировать фабрику, для её // использования другими процессами.// Создаём фабрику класса
hr = RealCFCreator::CreateInstance(
0,
IID_IClassFactory,
(void**)&m_pRealClassFactory);
if (hr == S_OK)
{
// Регистрируем её
hr = CoRegisterClassObject(
clsid,
m_pRealClassFactory,
CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER,
REGCLS_MULTIPLEUSE,
&m_dwRegister);
}
}
}
__finally
{
// Освобождение мьютекса. По уму это надо делать// через деструктор объекта CmyMutex, но в ATL такого// нет, а писать самостоятельно – лень...if (hMutex != 0)
{
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
}
return hr;
}
private:
DWORD m_dwRegister;
CComPtr<IClassFactory> m_pRealClassFactory;
};
|
Для облегчения использования этого класса предназначен следующий макрос:
#define MAKE_MACRO_PARAM(x, y) x, y
#define DECLARE_CLASSFACTORY_DLL_SINGLETON(obj) \
DECLARE_CLASSFACTORY_EX( \
MAKE_MACRO_PARAM( \
CComClassFactoryDllSingleton< \
obj, \
ATL::CComCreator< \
ATL::CComObjectCached< \
CComClassFactorySingleton< obj > > > > ))
|
| ПРИМЕЧАНИЕ Макрос MAKE_MACRO_PARAM предназначен для того, чтобы препроцессор истолковал CComClassFactoryDllSingleton< .., ..> как один параметр, а не как два параметра, разделённые запятой. За предложенное решение большое спасибо Андрею Солодовникову (Andrew S). Сергей Азаркевич (Sergey J.A.) предложил ввести макрос COMMA #define COMMA , и использовать его в выражении вместо запятых, не разделяющих параметры макроса. Это более общее решение, позволяющее беспрепятственно обойти любое количество «лишних» запятых, но в частном случае вариант Андрея смотрится понятнее и симпатичнее. |
В качестве «ядра» он использует стандартную ATL-фабрику для создания синглтонов.
Используется приведенная выше реализация фабрики классов элементарно, точно так же как стандартные макросы DECLARE_CLASSFACTORY. Достаточно добавить в тело класса, реализующего COM-объект, макрос DECLARE_CLASSFACTORY_DLL_SINGLETON.
class ATL_NO_VTABLE CTestObj :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CTestObj, &CLSID_TestObj>
{
public:
DECLARE_CLASSFACTORY_DLL_SINGLETON(CTestObj)
...
};
|
Описанная выше реализация работает, но у нее есть две серьёзные проблемы.
Проблема проявляется, если одновременно выполняются все следующие условия:
В этом случае мы имеем следующую картину:
| ПРЕДУПРЕЖДЕНИЕ Естественно, точно такая же проблема свойственна и стандартной ATL-реализации синглтона. И сама проблема, и класс, который её решает, описаны в «Q201321 HOWTO: Alternative Implementation of ATL Singleton». |
Есть два пути решения этой проблемы:
Поскольку описываемое поведение крайне нетипично для COM-объектов, скорее всего, процесс, загрузивший DLL, даже и не подозревает, что в нём находится синглтон. Соответственно, перед завершением он не будет заботиться о возможных внешних ссылках на синглтон, в результате чего все эти ссылки будут указывать в никуда. Или, выражаясь чуть более точно, возвращать одну из ошибок RPC_E_xxx (при проведении опытов было получено несколько разных значений).
Помимо очевидных организационных мер борьбы (написать специальный процесс, который будет создавать объект первым, и не будет выгружаться), можно попытаться пересмотреть архитектуру, сделав систему более распределённой. Идея заключается в следующем:
В результате, пока синглтон кому-то нужен, он будет практически бессмертен, как феникс, заново возрождающийся из пепла. И даже выключение питания компьютера не сможет прервать цепочку воплощений :)
Идея интересна, но её реализация выходит далеко за рамки статьи и оставляется читателю в качестве нетривиального развлечения, за которым можно провести не один долгий зимний вечер.
Оценка 70 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|