Сообщений 10 Оценка 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 (при проведении опытов было получено несколько разных значений).
Помимо очевидных организационных мер борьбы (написать специальный процесс, который будет создавать объект первым, и не будет выгружаться), можно попытаться пересмотреть архитектуру, сделав систему более распределённой. Идея заключается в следующем:
В результате, пока синглтон кому-то нужен, он будет практически бессмертен, как феникс, заново возрождающийся из пепла. И даже выключение питания компьютера не сможет прервать цепочку воплощений :)
Идея интересна, но её реализация выходит далеко за рамки статьи и оставляется читателю в качестве нетривиального развлечения, за которым можно провести не один долгий зимний вечер.
Сообщений 10 Оценка 70 Оценить |