Сообщений 5    Оценка 380        Оценить  
Система Orphus

Шаблон проектирования “Одиночка” (Singleton) в ATL приложениях

Автор: Иван Андреев
Aelita Software

Источник: RSDN Magazine #2-2003
Опубликовано: 03.08.2003
Исправлено: 13.03.2005
Версия текста: 1.0
Введение
Реализация “Синглтона” в библиотеке ATL
Синглтон в out-of-process сервере
Синглтон в in-process сервере
Альтернативные реализации синглтона
Реализация №1
Реализация №2
Типы маршалинга CoMarshalInterface
Синглтон во внепроцессном сервере
Синглтон во внутрипроцессном сервере
Реализация №3?
Заключение
Ссылки

Введение

Очень часто в конференциях, посвященных технологиям COM/DCOM и библиотеке ATL, можно встретить вопросы, так или иначе связанные с реализацией шаблона проектирования “Одиночка” (в дальнейшем в статье мы будем употреблять термин “Синглтон”) для COM-приложений и с проблемами, которые неизбежно возникают при таких реализациях. По популярности вопросы о синглтонах несильно отстают от классического вопроса “Мой проект в Release-сборке не компилируется – не найдена функция main”. В этой статье мы исследуем особенности существующих реализаций синглтона для библиотеки ATL, узнаем о потенциальных (и реальных) проблемах, с которыми сталкивается разработчик синглтона, и попробуем улучшить существующие реализации так, чтобы решить эти проблемы.

Описание шаблона проектирования “синглтон” очень простое – синглтон представляет собой единственный экземпляр класса, с которым работают все клиенты. Применительно к COM синглтон гарантирует, что все вызовы CoCreateInstance будут возвращать указатель на интерфейс единственного экземпляра компонента. Удобство использования таких компонентов/классов заключается в том, что все клиенты работают с одним и тем же экземпляром, а значит, получают доступ к разделяемому состоянию этого экземпляра.

Несмотря на простое описание, не существует “идеальной” реализации этого шаблона ни для C++, ни для COM-объектов. Связано это с тем, что любая существующая реализация имеет некоторые ограничения и не может выступать в роли “универсальной” реализации на все случаи жизни.

Природа проблем и ограничений существующих реализаций заключается в управлении временем жизни синглтона. Если время жизни обычного C++- или COM-объекта подчиняется вполне определенным правилам (например, время жизни COM-объекта определяется счетчиком ссылок), то в случае с синглтоном таких правил нет. Создание синглтона может происходить либо при старте программы, либо при первом обращении клиента – это очевидно и вопросов об этом не возникает. Но в какой момент синглтон должен быть уничтожен? На этот вопрос однозначного ответа нет. В некоторых случаях приемлемым решением может оказаться уничтожение синглтона в тот момент, когда у него не останется ни одного активного клиента. Если для C++-объектов такой принцип реализовать не очень легко, то COM-объекты с этой задачей справляются с лёгкостью, так как ведут подсчет ссылок. Другой подход к проблеме разрушения синглтона заключается в том, что синглтон будет уничтожен на этапе завершения программы, когда уже не может быть новых подключений клиентов. В первом случае может возникнуть проблема, связанная с тем, что, возможно, после разрушения синглтона найдется новый клиент, желающий получить доступ к синглтону. В такой ситуации синглтон должен быть создан заново (но предыдущее состояние будет уже потеряно), либо COM должен будет вернуть клиенту ошибку. Второй подход кажется более правильным, но и он содержит скрытую проблему – если во время своего разрушения синглтон использует какие-либо ресурсы (соединение с БД и т.п.), то эти ресурсы могут оказаться недоступными, так как разрушение происходит на очень поздней стадии завершения приложения.

Помимо перечисленных технических проблем, шаблон проектирования синглтон имеет еще один очень важный “идеологический” недостаток – при его применении абсолютно все клиенты вынуждены разделять не только общее состояние объекта синглтон, но и его реализацию. Такая система принципиально немасштабируема, так как с ростом количества клиентов все большая их часть будет простаивать, ожидая, пока будет закончено обслуживание предыдущих клиентов.

ПРИМЕЧАНИЕ

Однако, если количество клиентов синглтона невелико, этим можно пренебречь.

Реализация “Синглтона” в библиотеке ATL

Хорошая новость для тех, кто решил использовать шаблон проектирования “Синглтон” в ATL приложении, заключается в том, что библиотека ATL уже содержит его реализацию. Плохая новость – эта реализация имеет несколько очень серьезных недостатков, которые мы подробно разберем позже.

ПРИМЕЧАНИЕ

В этой статье мы будем использовать версию библиотеки ATL 3.0 и компилятор Visual C++ 6.0.

Сделать произвольный ATL COM-объект синглтоном очень просто – достаточно в объявлении класса добавить макрос

DECLARE_CLASSFACTORY_SINGLETON(CSingleton),

где CSingleton – наш СОМ-объект. Этот макрос раскрывается в следующее определение типа (typedef) внутри класса:

typedef CComCreator< CComObjectNoLock< 
  CComClassFactorySingleton<CSingleton> > > _ClassFactoryCreatorClass;

Для in-process - серверов вместо CComObjectNoLock будет использоваться класс CComObjectCached, который использует специальный механизм подсчета ссылок.

Это определение типа будет использовано в карте ATL-объектов. Обычно карта выглядит так:

BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_Singleton, CSingleton)
END_OBJECT_MAP()

Макрос OBJECT_ENTRY раскрывается в объявление структуры, содержащей информацию об ATL объекте и его фабрике классов:

struct _ATL_OBJMAP_ENTRY
{
	const CLSID* pclsid;
	HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister);
	_ATL_CREATORFUNC* pfnGetClassObject;
	_ATL_CREATORFUNC* pfnCreateInstance;
	IUnknown* pCF;
	DWORD dwRegister;
	_ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;
	_ATL_CATMAPFUNC* pfnGetCategoryMap;
	...
// Added in ATL 3.0
	void (WINAPI *pfnObjectMain)(bool bStarting);
};

В качестве pfnGetClassObject используется class::_ClassFactoryCreatorClass::CreateInstance. Это означает, что код ATL для получения фабрики классов будет вызывать статическую функцию class::_ClassFactoryCreatorClass::CreateInstance, где ClassFactoryCreatorClass задает конкретную фабрику, которая будет создавать экземпляры с помощью вызова pfnCreateInstance. pfnCreateInstance в свою очередь содержит адрес статической функции class::_CreatorClass::CreateInstance, где _CreatorClass обычно является синонимом класса CComCreator, который создает экземпляр класса и вызывает FinalConstruct.

Существование единственного экземпляра компонента достигается за счет использования специальной фабрики классов CComClassFactorySingleton (см. выше макрос DECLARE_CLASSFACTORY_SINGLETON).

Эта фабрика классов использует экземпляр другого вспомогательного класса CComObjectGlobal как переменную-член и возвращает его клиентам в реализации CreateInstance. Класс CComObjectGlobal реализует AddRef и Release через глобальный счетчик блокировок модуля – _Module.Lock и _Module.Unlock.

Таким образом, при использовании фабрики классов CComClassFactorySingleton экземпляр компонента хранится как переменная-член фабрики классов, а вызовы AddRef и Release для такого компонента приводят к увеличению/уменьшению счетчика блокировок модуля.

Такая реализация имеет очень существенные недостатки:

Синглтон в out-of-process сервере

Стандартный ATL-синглтон во внепроцессном сервере, как мы увидим далее, будет лишен части недостатков реализации CComClassFactorySingleton.

Рассмотрим небольшой пример ATL out-of-process сервера с синглтоном CSingleton, который создается с помощью “стандартного” макроса DECLARE_CLASSFACTORY_SINGLETON.

Код функции main, сгенерированный мастером выглядит так:

   if (bRun)
    {
        _Module.StartMonitor();
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
        hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,
            REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED);
        _ASSERTE(SUCCEEDED(hRes));
        hRes = CoResumeClassObjects();
#else
        hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER, 
            REGCLS_MULTIPLEUSE);
#endif
        _ASSERTE(SUCCEEDED(hRes));

        MSG msg;
        while (GetMessage(&msg, 0, 0, 0))
            DispatchMessage(&msg);

        _Module.RevokeClassObjects();
        Sleep(dwPause); //wait for any threads to finish
    }

Когда клиенты завершат работу с синглтоном, произойдет вызов _Module.RevokeClassObjects. Внутри этого метода произойдет разрушение фабрики классов синглтона, так как будет отпущена последняя и единственная ссылка на нее, которая была добавлена во время регистрации фабрики классов. Одновременно с разрушением фабрики классов произойдет разрушение синглтона, и проблема позднего разрушения синглтона не проявится. Кроме того, вызовы к фабрике классов от внешних клиентов всегда будут приходить в одном и том же апартаменте – в том, который был создан в функции main, поэтому проблема маршалинга синглтона также не проявится.

ПРИМЕЧАНИЕ

Здесь нужно подробнее остановиться на проблеме маршалинга. Фабрика классов вызывается инфраструктурой COM всегда в том апартаменте, в котором должен быть создан компонент. Это требование обусловлено тем, что сама фабрика обычно создает экземпляр с помощью new, и поэтому этот вызов уже должен происходить в нужном апартаменте. Это означает, что если клиент делает вызов CoCreateInstance в своем апартаменте, несовместимом с апартаментом компонента, COM “подыскивает” подходящий апартамент для компонента (или создает новый) и уже в нем вызывает фабрику классов для создания экземпляра. Для обычных компонентов вся эта процедура не имеет побочных эффектов, но синглтон создается однократно в одном апартаменте, а последующие вызовы к фабрике классов могут приходить из других апартаментов. Например, если синглтон имеет тип апартамента STA, и два клиента из разных STA создают его, второй вызов фабрики классов произойдет в апартаменте второго клиента. Клиент должен получить отмаршаленный указатель на синглтон, который уже был создан в другом STA. Стандартная фабрика классов ATL не делает этого, всегда возвращая прямой указатель.

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

С этим примером внепроцессного сервера не все так хорошо, как может показаться на первый взгляд. Синглтон представляет собой полноценный COM-объект, а создается он как переменная-член фабрики классов и поэтому, несмотря на ненулевой счетчик ссылок, разрушаться синглтон будет, когда произойдет разрушение фабрики классов, а для внепроцессного модуля это происходит при вызове _Module.RevokeClassObjects.

Представим себе такую ситуацию: клиент подключается к компоненту и вызывает метод, который выполняется длительное время, в этот момент по каким-либо причинам происходит остановка сервера и вызов _Module.RevokeClassObjects (например, если процесс представляет собой ATL-сервис, который останавливается командой “net stop”). Синглтон будет уничтожен, хотя в этот момент выполняется его метод (!!!). Самое лучшее, что может произойти после этого – аварийное завершение с ошибкой доступа к памяти.

ПРИМЕЧАНИЕ

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

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

Синглтон в in-process сервере

Для синглтонов во внутрипроцессных серверах проявятся все недостатки стандартной реализации, основной из которых – разрушение синглтона при выгрузке модуля - которое может привести к катастрофическим последствиям.

Фабрика классов синглтона для внутрипроцессного модуля создается и разрушается иначе, чем для внепроцессного. Причина различия заключается в том, что внепроцессные серверы регистрируют фабрику классов с помощью функции CoRegisterClassObject, а разрегистрируют с помощью CoRevokeClassObject. Для внутрипроцессных серверов фабрику классов должна вернуть экспортируемая из dll функция DllGetClassObject. Код ATL оптимизирует этот процесс, возвращая каждый раз один и тот же экземпляр фабрики классов. Чтобы достичь этого, используется специальный шаблон CComObjectCached, а кэшируемый интерфейс фабрики классов хранится в карте объектов ATL в поле IUnknown* pCF. Шаблон CComObjectCached реализует специальную схему подсчета ссылок. В этой схеме в том случае, если счетчик ссылок превышает 1, вызывается _Module.Lock, а когда он вновь становится равным 1 – _Module.Unlock. Это необходимо, чтобы кэшируемая в карте объектов ATL ссылка на фабрику классов не приводила к блокировке всего модуля. Ссылка IUnknown* pCF будет освобождена только при вызове _Module.Term, которая для dll вызывается при выгрузке модуля. Это, в свою очередь, означает, что и синглтон, хранящийся как переменная-член фабрики классов, будет разрушен только во время выгрузки модуля. Осталось заметить, что выгрузка in-process сервера, как правило, происходит при вызове клиентом CoUninitialize.

Такой синглтон в in-process сервере может доставить массу хлопот:

class ATL_NO_VTABLE CSingletonA : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CSingletonA, &CLSID_SingletonA>,
  public IDispatchImpl<ISingletonA, &IID_ISingletonA, &LIBID_STDSINGLETONINLib>
{
public:
  CSingletonA()
  {
  }

  HRESULT FinalConstruct()
  {
    return m_spDoc.CoCreateInstance(CLSID_DOMDocument);
  }
  void FinalRelease()
  {
    m_spDoc.Release();
  }

  DECLARE_CLASSFACTORY_SINGLETON(CSingletonA)
  ...

private:
  CComPtr<IXMLDOMDocument> m_spDoc;
};

В приведенном примере синглтон пытается при разрушении освободить ссылку на документ MSXML. Если вызов FinalRelease() происходит внутри CoUninitialize (а так и будет, если клиент явно не вызвал CoFreeUnusedLibraries), то попытка вызова m_spDoc.Release() может привести к ошибке доступа к памяти, если модуль msxml.dll уже был выгружен раньше.

ПРЕДУПРЕЖДЕНИЕ

Неприятная особенность этой проблемы заключается в том, что иногда (когда порядок выгрузки модулей “правильный”) код будет работать, а при некоторых обстоятельствах – “рушиться” на вызове m_spDoc.Release(). Другими словами, ошибка, как часто говорят, будет «плавающей», а «плавающие» ошибки искать значительно труднее, чем проявляющиеся постоянно.

Проблема маршалинга синглтона в фабрике классов также весьма актуальна для внутрипроцессных серверов, так как клиенты запросто могут находиться в разных апартаментах, и все клиенты будут получать прямую ссылку на синглтон. Для синглтонов с потоковой моделью Apartment это может означать, что вызовы могут приходить из других потоков. Без синхронизации все закончится плачевно, а клиенты даже не будут ничего подозревать, так как имеют дело не с proxy, а с прямым указателем, и ошибку RPC_E_WRONGTHREAD, означающую, что вызов был сделан из “неподходящего” потока, им никто не вернет.

Для синглтонов с потоковой моделью Apartment в in-process серверах есть еще одна специфическая проблема – STA, в котором синглтон был создан, должен быть «живым» до момента разрушения синглтона. Для обычных компонентов это требование вполне естественно, но клиенты синглтона ничего не знают о других клиентах и о том апартаменте, в котором синглтон был создан. Представим себе такой сценарий. Клиент C1 создает синглтон в своем STA и работает с ним, после этого клиент C2 получает ссылку на синглтон в другой STA и работает с ним, тем временем, клиент C1 освобождает свою ссылку и уничтожает свой апартамент вызовом CoUninitialize(). Если клиент C2 продолжит работу с синглтоном – результат может оказаться плачевным, так как апартамента синглтона больше не существует.

Альтернативные реализации синглтона

Стандартная реализация вполне приемлема для внепроцессных серверов, но совсем не подходит для внутрипроцессных, – если с проблемой маршалинга и можно было бы смириться (не используя Apartment синглтоны), то для проблемы разрушения синглтона внутри CoUninitialize нужно искать решение.

Как мы уже упоминали выше, существует две стратегии для разрушения синглтона:

Стандартный ATL-синглтон использует первую стратегию и, как мы увидели, это приводит к нежелательным последствиям, так как выгрузка модуля dll обычно происходит внутри вызова CoUninitialize. Таким образом, чтобы решить проблему CoUninitialize, нам придется отказаться от этой стратегии и использовать альтернативную, хотя для внепроцессных серверов подходят обе.

Реализация №1

Наша первая реализация будет использовать стратегию разрушения синглтона, при которой синглтон освобождается, как только клиент отпускает последнюю ссылку. В мире C++ такие синглтоны принято называть Phoenix за способность “возрождаться” после разрушения. Побочный эффект этой стратегии связан с тем, что синглтон теряет свое состояние после “возрождения”.

ПРИМЕЧАНИЕ

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

Мы будем использовать шаблон CComObjectPh, который унаследован от CcomObject. Новый метод, реализованный в этом шаблоне, во время своего создания передает фабрике классов указатель, по которому будут производиться последующие обращения. Во время разрушения объекта указатель, хранящийся в фабрике, обнуляется, сигнализируя тем самым о том, что при очередном обращении нужно создать новый экземпляр синглтона.

ПРИМЕЧАНИЕ

Приведенный ниже код использует предположение о том, что время жизни любого компонента вложено во время жизни его фабрики классов. Это верно для ATL, так как фабрика классов разрушается внутри _Module.Term или при вызове _Module.RevokeClassObject. В любом случае, это происходит только тогда, когда у модуля нет блокировок, а это, в свою очередь, возможно только при отсутствии “живых” компонентов. Поэтому время жизни компонента вложено во время жизни его фабрики, и мы можем при разрушении компонента обращаться к переменным-членам фабрики.

template <class Base>
class CComObjectPh : public CComObject<Base>
{
public:
  STDMETHOD_(ULONG, Release)()
  {
    LPCRITICAL_SECTION pcs = m_pcs;
    ::EnterCriticalSection(pcs);

    ULONG l = InternalRelease();

    if (l == 0)
    {
      *m_ppObj = 0; // обнуляем указатель, который хранит фабрика
      delete this;
    }
    ::LeaveCriticalSection(pcs);
    return l;
  }

  static HRESULT WINAPI CreateInstance(CComObjectPh<Base>** pp, 
    LPCRITICAL_SECTION pcs)
  {
    // создаем новый экземпляр
    ATLASSERT(pp != NULL);
    HRESULT hRes = E_OUTOFMEMORY;
    CComObjectPh<Base>* p = NULL;
    ATLTRY(p = new CComObjectPh<Base>())
    if (p != NULL)
    {
      p->SetVoid(NULL);
      p->InternalFinalConstructAddRef();
      hRes = p->FinalConstruct();
      p->InternalFinalConstructRelease();
      if (hRes != S_OK)
      {
        delete p;
        p = NULL;
      }
    }
    *pp = p;

    if(SUCCEEDED(hRes))
    {
      // сохраняем указатели для фабрики
      (*pp)->m_ppObj = pp;
      (*pp)->m_pcs = pcs;
    }
    return hRes;
  }

  CComObjectPh<Base>** m_ppObj;
  LPCRITICAL_SECTION m_pcs;
};


template <class T>
class CComClassFactoryPhSingleton : public CComClassFactory
{
public:
  CComClassFactoryPhSingleton() : m_pObj(0) {}
  void FinalRelease()
  {
    if(m_pObj)
      ::CoDisconnectObject(m_pObj->GetUnknown(), 0);
  }

  // IClassFactory
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
  {
    HRESULT hRes = S_OK;
    if (ppvObj != NULL)
    {
      *ppvObj = NULL;

      m_cs.Lock();

      if (m_pObj == 0)
      {
        hRes = CComObjectPh<T>::CreateInstance(&m_pObj,
          &m_cs.m_sec );
      }
                
      if(SUCCEEDED(hRes))
      hRes = m_pObj->QueryInterface(riid, ppvObj);
      m_cs.Unlock();
    }
    return hRes;
  }
  CComObjectPh<T>* m_pObj;
  CComAutoCriticalSection m_cs;
};

На этом мы могли бы остановиться, но… существует еще проблема маршалинга синглтона, которую этот код не решает. Все клиенты, независимо от их апартамента, по-прежнему получают прямые указатели на синглтон. Это справедливо только для случая, когда синглтон имеет тип потоковой модели Apartment. Для Free-синглтонов проблемы не существует, так как:

Но как быть, если синглтон по тем или иным причинам должен иметь тип потоковой модели Apartment? В этом случае нам нужно обеспечить способ передачи клиенту корректного указателя. Для этого придется использовать маршалинг.

Реализация №2

Чтобы решить проблему маршалинга, нужно в фабрике классов для уже созданного синглтона иметь не обычный указатель CComObjectPh<T>* m_pObj, а нечто, с помощью чего можно получить корректно отмаршаленный указатель на синглтон, который может находиться не в том апартаменте, где происходит вызов IClassFactory::CreateInstance.

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

ПРИМЕЧАНИЕ

Статья в MSDN Q201321 “HOWTO: Alternative Implementation of ATL Singleton”

   STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
                             void** ppvObj)
   {
	   ...
      if (NULL == m_pObj)
      {
         hRes = CComObjectEx<T>::CreateInstance(&m_pObj);
         if (SUCCEEDED(hRes))
         {
		    ...
              // Marshal interface into a stream.
              // Can't use AtlMarshalPtrInProc since we need to use
              // MSHLFLAGS_TABLEWEAK.
              hRes = CreateStreamOnHGlobal(NULL, TRUE, &m_pStream);
              if (SUCCEEDED(hRes))
              {
                 hRes = CoMarshalInterface(m_pStream, IID_IUnknown,
                         m_pObj->GetUnknown(), MSHCTX_INPROC | MSHCTX_LOCAL,
                         NULL, MSHLFLAGS_TABLEWEAK);
                 if (FAILED(hRes))
                 {
                    m_pStream->Release();
                    m_pStream = NULL;
                 }
              }
          }
      }
      CComPtr<IUnknown> spUnk;
      // Unmarshal interface from stream.
      hRes = AtlUnmarshalPtr(m_pStream, IID_IUnknown, &spUnk);
	  ...
      return hRes;
   }

Фабрика классов хранит не указатель на экземпляр класса CComObject, а приготовленный к маршалингу пакет (полученный вызовом CoMarshalInterface). Когда поступает запрос на создание компонента – вызывается функция AtlUnmarshalPtr, которая и получает корректный указатель для текущего апартамента.

Если рассмотреть код внимательно, то можно заметить, что функция CoMarshalInterface вызывается с флагом MSHLFLAGS_TABLEWEAK. Давайте попробуем разобраться, почему в данном случае нужен такой тип маршалинга.

Типы маршалинга CoMarshalInterface

API функция CoMarshalInterface предназначена для маршалинга интерфейса в другой апартамент.

STDAPI CoMarshalInterface(
//Указатель на интерфейс IStream*, в который будет помещен пакет
IStream *pStm, 
//IID интерфейса
REFIID riid, 
//Указатель на интерфейс для маршалинга
IUnknown *pUnk,
//Контекст маршалинга
DWORD dwDestContext, 
//Зарезервировано
void *pvDestContext, 
//Тип маршалинга
DWORD mshlflags 
);

Параметр pStm задает поток IStream, в который сохраняется пакет для получения корректной ссылки в другом апартаменте, riid и pUnk описывают интерфейс, маршалинг которого производится, dwDestContext задает контекст маршалинга ( внутрипроцессный , внепроцессный и т.п.).

Обычная схема маршалинга – “один к одному”. Это означает, что в апартаменте компонента происходит вызов CoMarshalIterface, в другой апартамент передается пакет Istream, после чего происходит однократный вызов CoUnmarshalInterface. Это обычный маршалинг, и он задается значением последнего параметра mshlflags равным MSHLFLAGS_NORMAL.

Однако бывают случаи, когда необходимо обеспечить схему “один ко многим”, т.е. несколько клиентов должны иметь возможность вызывать CoUnmarshalInterface для получения корректного указателя на интерфейс. Такой маршалинг называется табличным и смысл его заключается в том, что информация о маршалируемом интерфейсе помещается в глобальную таблицу, и клиенты могут получать указатели на интерфейс вызовом CoUnmarshalInterface неоднократно.

Для синглтона пригоден только табличный маршалинг, Фабрика классов должна иметь возможность получить для каждого клиента отмаршаленный указатель на интерфейс, многократно обращаясь для этого к функции CoUnmarshalInterface. CoMarshalInterface, в свою очередь, вызывается один раз при создании синглтона в его “родном” апартаменте. Таким образом, в случае синглтона нам необходима схема маршалинга “один ко многим”.

Табличный маршалинг CoMarshalInterface представлен в двух разновидностях (которые задаются значением параметра mshlflags):

Как следует из названия, мы имеем дело с сильными и слабыми ссылками.

ПРИМЕЧАНИЕ

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

MSHLFLAGS_TABLESTRONG задает режим работы маршалинга, при котором полученный вызовом CoMarshalInterface пакет в IStream представляет собой ссылку на маршалируемый интерфейс, для которой был вызван AddRef(). Поэтому компонент не будет уничтожен до тех пор, пока не будет сделан вызов CoReleaseMarshalData. Этот режим не может использоваться в тех случаях, когда невозможно определить момент, в который должен быть сделан вызов CoReleaseMarshalData. Сам компонент не может осуществить такой вызов, так как время его жизни контролируется пакетом. Объект не будет разрушен, пока кто-нибудь другой не вызовет для него CoReleaseMarshalData. В таких случаях нужно использовать другое значение флага mshlflags, MSHLFLAGS_TABLEWEAK. При таком маршалинге пакет, созданный вызовом CoMarshalInterface, не удерживает компонент от разрушения, а попытки маршалинга с помощью CoUnmarshalInterface после разрушения объекта будут просто возвращать ошибку.

Понятно, что для синглтона нам нужно использовать именно второй режим, так как вызвать CoReleaseMarshalData некому.

ПРИМЕЧАНИЕ

CoReleaseMarshalData могла бы вызывать фабрика классов, но она сможет сделать это только при собственном разрушении, а происходит это, как мы уже разобрались, внутри CoUninitialize().

Для синглтона использование MSHLFLAGS_TABLEWEAK должно привести к тому, что когда все клиенты отпустят свои ссылки на него, он будет разрушен, а фабрика классов, обнаружив это, создаст новый экземпляр компонента.

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

Синглтон во внепроцессном сервере

Мы создадим внепроцессный сервер ATL с синглтоном в нем, использующим реализацию из KB MSDN.

DECLARE_CLASSFACTORY_SINGLETONMARSH(CSingleton)

Тестовый клиент будет реализован на VBScript:

set o = CreateObject("StdSingletonOut.Singleton.1")
set o = Nothing

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

hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE);
// создаем синглтон и ждем 10 секунд
CComPtr<ISingleton> spSingle;
spSingle.CoCreateInstance(CLSID_Singleton);
HANDLE hTimer = ::CreateWaitableTimer(0, TRUE, 0);
        
const long second = 10000000;
LARGE_INTEGER dueTime = { -second*10, -1};
      
::SetWaitableTimer(hTimer, &dueTime, 0, 0, 0, FALSE);
AtlWaitWithMessageLoop(hTimer);
spSingle.Release();

MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);

В приведенном примере синглтон создается внутри процесса сервера и ссылка на него удерживается в течение 10 секунд. Если за это время сервер получит такой запрос от клиента:

set o = CreateObject("StdSingletonOut.Singleton.1")
set o = Nothing
set o = CreateObject("StdSingletonOut.Singleton.1")
set o = Nothing

то попытка повторного создания закончится с ошибкой COE_OBJNOTCONNECTED.

Получается, что если синглтон обрабатывает не только обращения от внешних клиентов, но и внутренние вызовы AddRef, то внешние клиенты начнут получать ошибку CO_E_OBJNOTCONNECTED. Попробуем разобраться, почему так происходит.

На самом деле, флаг маршалинга MSHLFLAGS_TABLEWEAK лишь косвенно влияет на счетчик ссылок самого компонента. Вспомним, что в процессе вызова компонента из другого апартамента участвуют еще несколько компонентов из инфраструктуры COM. Со стороны клиента компонент представляет Proxy или Заместитель, а со стороны компонента – Stub или Заглушка. Для каждого интерфейса создается своя собственная заглушка, а управляет ими Администратор Заглушек (Stub Manager). Чтобы удерживать компонент от разрушения, Администратор Заглушек всегда удерживает, по крайней мере, одну ссылку (на самом деле несколько ссылок). Флаги маршалинга MSHLFLAGS_TABLEWEAK и MSHLFLAGS_TABLESTRONG влияют на начальное значение счетчика ссылок именно Администратора Заглушек, а он, в свою очередь, независимо от этих флагов всегда удерживает сильную ссылку на компонент.

Как следует из названия флагов, MSHLFLAGS_TABLESTRONG приводит к созданию Администратора со счетчиком ссылок равным 1, и чтобы он мог быть разрушен (а вместе с ним и удерживаемый компонент), необходимо вызвать функцию CoReleaseMarshalData, которая и освободит эту ссылку на Администратор. MSHLFLAGS_TABLEWEAK, напротив, создает Администратор Заглушек с нулевым (!) количеством ссылок, а сам Администратор вызывает AddRef компонента. Таким образом, после вызова CoMarshalInterface с этим флагом счетчик ссылок компонента все равно увеличится. Так в чем же польза применения этого флага? Представим, что к компоненту подключается клиент из другого процесса. Это приведет к увеличению на 1 счетчика ссылок Администратора Заглушек. Когда клиент закончит работу с интерфейсом, то произойдет вызов Release(). В этой ситуации, если изначально счетчик ссылок Администратора был равен 0, он будет разрушен. Вместе с ним разрушится удерживаемый компонент. Именно это поведение и нужно для синглтона – когда внешний клиент заканчивает работу с синглтоном, разрушается Администратор Заглушек, а вместе с ним и сам синглтон. Но предположим, что счетчик ссылок самого синглтона больше 1, т.е. существуют внутренние клиенты, которые удерживают свои ссылки на синглтон. В этой ситуации, когда последний внешний клиент отпускает свою ссылку, Администратор Заглушек разрушается, а синглтон – нет. Фабрика классов продолжает считать (и вполне обоснованно), что синглтон в полном порядке, и когда подключается очередной внешний клиент, вызывает AtlUnmarshalPtr, но Администратора Заглушек уже не существует, именно поэтому вызов заканчивается с ошибкой CO_E_OBJNOTCONNECTED.

Итак, реализация №2 для внепроцессных серверов работает корректно только при условии, что у синглтона нет клиентов внутри процесса сервера.

ПРИМЕЧАНИЕ

Если быть абсолютно точным, речь идет о внутренних клиентах, которые находятся в одном апартаменте с синглтоном, т.е. их вызовы AddRef и Release минуют Администратор Заглушек, не влияя на его время жизни.

Синглтон во внутрипроцессном сервере

Теперь попробуем использовать эту реализацию синглтона во внутрипроцессном сервере. В качестве клиента мы будем использовать VBScript, а синглтон будет представлен в двух вариантах – с потоковой моделью Apartment(SingletonA) и Free(SingletonF).

После запуска примера:

Dim oa As STDSINGLETONINLib.SingletonA
Set oa = New STDSINGLETONINLib.SingletonA
Set oa = Nothing

Dim of As STDSINGLETONINLib.SingletonF
Set of = New STDSINGLETONINLib.SingletonF
Set of = Nothing

Мы увидим, что для Free-синглтона все работает так, как и должно, а вот вызов FinalRelease() у компонента SingletonA все равно происходит внутри CoUninitialize со всеми вытекающими последствиями. Причина таких отличий кроется в том, что клиент в данном случае находился в STA, и первый компонент использовался напрямую, а для второго применялся маршалинг из MTA в STA. Нетрудно догадаться, что всему виной был снова … Администратор Заглушек. Вспомним, что при использовании флага MSHLFLAGS_TABLEWEAK Администратор Заглушек получает 0 начальных ссылок, так как клиент и компонент SingletonA находятся в одном апартаменте. Нет маршалинга, а, значит, и вызовы AddRef и Release минуют Администратор Заглушек, следовательно он не разрушается до вызова CoUninitialize, а вместе с ним не разрушается и сам синглтон. Для компонента SingletonF, AddRef и Release проходят через Администратор Заглушек, и проблемы не возникает, он разрушается сразу же, как только клиент отпускает свою ссылку.

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

#include "stdafx.h"
#include "process.h"

unsigned __stdcall Th(LPVOID param)
{
  CoInitializeEx(0, COINIT_APARTMENTTHREADED);
  {
    static CComAutoCriticalSection cs;
    cs.Lock();
    ISingletonFPtr spF;
    HRESULT hr = spF.CreateInstance(CLSID_SingletonF);
    if(spF)
      spF.Release();
    cs.Unlock();

    if(FAILED(hr))
    {
      DebugBreak();
    }

  }
  CoUninitialize();
  return 0;
}
int main(int argc, char* argv[])
{
  CoInitializeEx(0, COINIT_MULTITHREADED);
  {
    ISingletonFPtr spF(CLSID_SingletonF);
    const long N = 3;
    HANDLE h[N];
    for(int i = 0; i < N; ++ i)
      h[i] = (HANDLE)_beginthreadex(0, 0, Th, 0, 0, 0);
    WaitForMultipleObjects(N, h, TRUE, INFINITE);
  }
  CoUninitialize();
  return 0;
}

Этот пример сначала создает синглтон так, чтобы не было маршалинга, то есть в апартаменте MTA, а после этого несколько раз пытается создать его вновь из разных STA. Как и следовало ожидать, попытки обратиться к Администратору Заглушек после его разрушения приводят к ошибке CO_E_OBJNOTCONNECTED.

Реализацию №2 нельзя считать удачной, потому что проблемы, связанные с ее использованием, оказались даже серьезнее, чем для стандартной реализации ATL. Клиенту не гарантируется, что в ответ на обращение к синглтону он не получит ошибку CO_E_OBJNOTCONNECTED. Можно возразить, что существуют сценарии, когда эта реализация будет работать вполне корректно. Во-первых, у синглтона не должно быть клиентов внутри его апартамента. Второе требование вытекает из первого – между клиентом и синглтоном всегда должен быть маршалинг. Даже если эти ограничения выполняются в отдельно взятом проекте, никто не может гарантировать, что они не будут нарушены во время его модификаций и сопровождения. Правила COM таковы, что клиент не может и не должен знать, где находится компонент, в том же апартаменте или нет.

ПРИМЕЧАНИЕ

Если желание использовать именно эту реализацию не пропало после рассмотрения ее проблем, можно предложить обходной путь, который поможет избавиться от ошибки CO_E_OBJNOTCONNECTED и позднего разрушения синглтона внутри CoUninitialize. Этот обходной путь опирается на использование функции API CoLockObjectExternal, с помощью которой можно увеличивать/уменьшать счетчик ссылок Администратора Заглушек. Если после первого создания синглтона вызвать CoLockObjectExternal(TRUE), а перед последним уничтожением CoLockObjectExternal(FALSE), то обе проблемы решатся. Главная сложность этого подхода состоит в том, что как правило, очень сложно определить моменты первого создания и последнего разрушения синглтона.

Реализация №3?

КВ не помог нам решить проблему маршалинга синглтона, так как у маршалинга с флагом MSHLFLAGS_TABLEWEAK есть особенность, связанная с прямыми вызовами компонента. При таких вызовах счетчик ссылок у Администратора Заглушек не увеличивается, в результате мы получаем либо неосвобожденный синглтон, либо ошибку CO_E_OBJNOTCONNECTED для клиентов из других апартаментов.

Необходимо отметить, что проблема существует только для Apartment-синглтонов, для Free вполне достаточно использовать реализацию №1 без маршалинга.

Но как же быть, если синглтон по каким-либо причинам должен иметь тип потоковой модели Apartment?

Рассмотрим несколько идей, как реализовать фабрику классов синглтона для потоковой модели Apartment:

Возможно, перечисленные проблемы и решаемы, но на сегодняшний день мне неизвестна реализация COM-синглтона, которая бы корректно/надежно работала в потоковой модели Apartment.

ПРИМЕЧАНИЕ

Необходимо отметить, что у Apartment-синглтонов есть еще проблема разрушения “родного” апартамента. Это означает, что нужно обязательно гарантировать, что апартамент STA, в котором синглтон был создан, будет существовать вплоть до конца работы с синглтоном.

Заключение

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

РеализацияДостоинстваНедостатки/Ограничения
Стандартная ATLВходит в состав ATL и, возможно, будет когда-нибудь исправлена/улучшена1) Если синглтон не был создан при первом обращении, он не будет создан никогда. 2) Фабрика классов не выполняет маршалинг для синглтона в другие апартаменты. 3) Синглтон создается как переменная-член фабрики классов, поэтому время его жизни контролируется не счетчиком ссылок, а временем жизни фабрики. 4) Синглтон разрушается при выгрузке модуля, что приводит к проблеме корректного освобождения ресурсов. Реализация непригодна для использования ни во внепроцессных, ни во внутрипроцессных серверах
Реализация №1.“Возрождающийся” синглтон без маршалингаПростаяНе подходит для внутрипроцессных Apartment-синглтонов, так как не выполняет маршалинга в фабрике классов
Реализация №2 из KB MSDN-1) у синглтона не должно быть клиентов в его апартаменте.
2) между любым клиентом и синглтоном всегда должен производиться маршалинг (вытекает из первого требования).Малопригодна для внепроцессных северов и практически непригодна для внутрипроцессных.
Таблица 1

Следующая таблица содержит сведения о применимости реализаций для каждого типа сервера и потоковой модели синглтона.

Тип синглтона/РеализацияСтандартная ATLРеализация №1 “возрождающегося” синглтона без маршалингаРеализация №2 из KB MSDN
Внутрипроцессный (dll) Aparttment Только если разрушение синглтона не связано с освобождением ресурсов, так как разрушение происходит при вызове CoUninitialize. Ресурсами являются и указатели на интерфейсы других компонентов.Клиенты из разных STA будут получать прямой указатель, так как фабрика классов не выполняет маршалинга – поэтому методы синглтона могут выполняться параллельно. Клиенты из разных STA будут получать прямой указатель, так как фабрика классов не выполняет маршалинг – поэтому методы синглтона могут выполняться параллельно.Не должно быть клиентов в апартаменте синглтона. При наличии таких клиентов синглтон либо будет разрушаться внутри вызова CoUninitialize, либо внешние клиенты из других апартаментов будут получать ошибку CO_E_OBJNOTCONNECTED.
Внутрипроцессный (dll) FreeТолько если разрушение синглтона не связано с освобождением ресурсов, так как разрушение происходит при вызове CoUninitialize. Работает корректно.Не должно быть клиентов в апартаменте синглтона. При наличии таких клиентов синглтон либо будет разрушаться внутри вызова CoUninitialize, либо внешние клиенты из других апартаментов будут получать ошибку CO_E_OBJNOTCONNECTED.
Внепроцессный (exe) ApartmentНужно модифицировать код сервера, чтобы перед вызовом CoRevokeClassObjects количество блокировок модуля было равно 0 – так как время жизни синглтона контролируется фабрикой классов. Внутри процесса не должно быть клиентов из других STA, так как фабрика не выполняет маршалинга.Внутри процесса не должно быть клиентов из других STA, так как фабрика не выполняет маршалинг. Для внешних по отношению к процессу клиентов в любых апартаментах работает корректно.Не должно быть клиентов внутри процесса в апартаменте синглтона. При наличии таких клиентов синглтон либо будет разрушаться внутри вызова CoUninitialize, либо внешние клиенты из других апартаментов будут получать ошибку CO_E_OBJNOTCONNECTED
Внепроцессный (exe) FreeНужно модифицировать код сервера, чтобы перед вызовом CoRevokeClassObjects количество блокировок модуля было равно 0.Работает корректноНе должно быть клиентов внутри процесса в апартаменте синглтона. При наличии таких клиентов синглтон либо будет разрушаться внутри вызова CoUninitialize, либо внешние клиенты из других апартаментов будут получать ошибку CO_E_OBJNOTCONNECTED
Таблица 2. Применимость реализаций

В результате мы видим, что для синглтонов с потоковой моделью Free оптимальной является Реализация №1, так как с ее использованием не связано каких-либо проблем. Хуже всего дела обстоят с Apartment-синглтонами во внутрипроцессном сервере – ни одна из перечисленных реализаций для них не подходит. Хотя, если сервер внепроцессный, и у синглтона нет клиентов в том же апартаменте, можно пытаться использовать синглтон из KB MSDN, но я бы этого делать не советовал – если такие клиенты появятся, придется менять архитектуру приложения, по меньшей мере, потоковую модель синглтона.

ПРИМЕЧАНИЕ

Даже если бы существовала “идеальная” реализация шаблона проектирования “Одиночка” для потоковой модели Apartment, такой синглтон показывал бы очень плохую производительность, так как все клиенты, независимо от их типа апартамента, должны были бы использовать маршалинг, чтобы вызывать методы компонента.

Если нужно создать синглтон COM-объект, который должен создаваться и работать в апартаменте клиента, то есть иметь потоковую модель Both, то лучше всего реализовывать такой объект по методу 1 и агрегировать в нем FTM (Free Threaded Marshaler). Это избавит от проблем с маршалингом и обеспечит наивысшую скорость взаимодействия с объектом (так как в процессе межапартаментного маршалинга клиент всегда будет получать прямую ссылку на объект). FTM можно применять и для STA-компонентов, но при этом не следует забывать, что никакой синхронизации вызовов к объекту не будет. В этом случае STA будет не более чем видимостью.

Компоненты, агрегирующие FTM (вне зависимости от апартаментной модели), требуют ручной синхронизации доступа к своим данным вне зависимости от объявленной потоковой модели.

И в заключение хочется отметить, что не следует ожидать корректной/устойчивой работы синглтона в COM+, но это – тема отдельной статьи.

Ссылки

Статья в MSDN Q201321 “HOWTO: Alternative Implementation of ATL Singleton”


Эта статья опубликована в журнале RSDN Magazine #2-2003. Информацию о журнале можно найти здесь
    Сообщений 5    Оценка 380        Оценить