Сущность технологии COM

       

Снова о времени жизни сервера


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

// reasons to remain loaded // причины оставаться загруженными LONG g_cLocks = 0; // called from AddRef + IClassFactory::LockServer(TRUE) // вызвано из AddRef + IClassFactory::LockServer(TRUE) void LockModule(void) { InterlockedIncrement(&g_cLocks); } // called from Release + IClassFactory::LockServer(FALSE) // вызвано из Release + IClassFactory::LockServer(FALSE) void UnlockModule(void) { InterlockedDecrement(&g_cLocks); }

Это сделало реализацию DllCanUnloadNow предельно простой:

STDAPI DllCanUnloadNow() { return g_cLocks ? S_FALSE : S_OK; }

Подпрограмму DllCanUnloadNow нужно вызывать в случаях, когда клиент решил "собрать мусор" в своем адресном пространстве путем вызова CoFreeUnusedLibraries для освобождения неиспользуемых библиотек.

Имеются некоторые различия в том, как ЕХЕ-серверы прекращают работу серверов. Во-первых, обязанностью серверного процесса является упреждающее инициирование процесса своего выключения. В отличие от внутрипроцессных серверов, здесь не существует "сборщика мусора", который запросил бы внепроцессный сервер, желает ли он прекратить работу. Вместо этого серверный процесс должен в подходящий момент явно запустить процесс своего выключения. Если для выключения сервера используется событие Win32 Event, то процесс должен вызвать API-функцию SetEvent:


void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) ==0) { extern HANDLE g_heventShutdown; SetEvent(g_heventShutdown); } }

Если вместо серверного основного потока обслуживается очередь событий Windows MSG, то для прерывания цикла обработки сообщений следует использовать некоторые из API-функций. Проще всего использовать PostThreadMessage для передачи в основной поток сообщения WM_QUIT:

void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) == 0) { extern DWORD g_dwMainThreadID; // set from main thread // установлено из основного потока PostThreadMessage(g_dwMainThreadID, WNLQUIT, О, 0); } }

Если серверный процесс на основе STA знает, что он никогда не будет создавать дополнительные потоки, то он может использовать несколько более простую API-функцию PostQuitMessage:

void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) == 0) PostQuitMessage(0); }

Этот способ работает только при вызове из главного потока серверного процесса.



Второе различие в управлении временем жизни внутрипроцессного и внепроцессного сервера связано с тем, что должно поддерживать сервер в загруженном или работающем состоянии. В случае внутрипроцессного сервера такой силой обладают неосвобожденные ссылки на объекты и неотмененные вызовы IClassFactory::LockServer(TRUE). Неосвобожденные ссылки на объекты необходимо рассмотреть в контексте внепроцессного сервера.

Безусловно, сервер должен оставаться доступным до тех пор, пока внешние клиенты имеют неосвобожденные ссылки на объекты класса сервера. Для внутрипроцессного сервера это реализуется следующим образом:

STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) { LockModule(); // note outstanding reference // отмечаем неосвобожденную ссылку return 2; // non-heap-based object // объект, размещенный не в "куче" }

STDMETHODIMP_(ULONG) MyClassObject::Release(void) { UnlockModule(); // note destroyed reference // отмечаем уничтоженную ссылку return 1; // non-heap-based object // объект, размещенный не в "куче" }



Такое поведение является обязательным, поскольку если DLL выгружается, несмотря на оставшиеся неосвобожденные ссылки на объекты класса, то даже последующие вызовы метода Release приведут клиентский процесс к гибели.

К сожалению, предшествующая реализация AddRef и Release не годится для внепроцессных серверов. Напомним, что после входа в апартамент COM первое, что делает типичный внепроцессный сервер, — регистрирует свои объекты класса с помощью библиотеки COM путем вызова CoRegisterClassObject. Тем не менее, пока таблица класса сохраняет объект класса, существует по меньшей мере одна неосвобожденная ссылка COM на объект класса. Это означает, что после регистрации своих объектов класса счетчик блокировок всего модуля будет отличен от нуля. Эти самоустановленные (self-imposed) ссылки не будут освобождены до вызова серверным процессом CoRevokeClassObject. К сожалению, типичный серверный процесс не вызовет CoRevokeClassObject до тех пор, пока счетчик блокировок всего модуля не достигнет нуля, что означает, что серверный процесс никогда не прекратится.

Чтобы прервать циклические отношения между таблицей класса и временем жизни сервера, большинство внепроцессных реализации объектов класса попросту игнорируют неосвобожденные ссылки на AddRef и Release:

STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) { // ignore outstanding reference // игнорируем неосвобожденную ссылку return 2; // non-heap-based object // объект, размещенный не в "куче" }

STDMETHODIMP_(ULONG) MyClassObject::Release(void) { // ignore destroyed reference // игнорируем уничтоженную ссылку return 1; // non-heap-based object //объект, размещенный не в "куче" }

Это означает, что после регистрации объектов своего класса счетчик блокировок всего модуля останется на нуле.

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


Напомним, что сервер должен продолжать работу до тех пор, пока на объекты его класса есть внешние ссылки. Предшествующие модификации AddRef и Release влияют только на внутренние ссылки, которые хранятся в таблице классов библиотеки COM и поэтому игнорируются. Когда внешний клиент запрашивает ссылку на один из объектов класса серверного процесса, SCM входит в апартамент объекта класса для отыскания там ссылки на объект класса. В это время делается вызов CoMarshalInterface для сериализации объектной ссылки с целью использования ее клиентом. Если объект класса реализует интерфейс IExternalConnection, то он может заметить, что внешние ссылки являются неосвобожденными, и использовать эти сведения для управления временем жизни сервера. Если предположить, что объект класса реализует интерфейс IExternalConnection, тo следующий код достигает желаемого эффекта:

STDMETHODIMP_(DWORD) MyClassObject::AddConnection(DWORD extconn, DWORD) { DWORD res = 0; if (extconn & EXTCONN_STRONG) { LockModule(); // note external reference // записываем внешнюю ссылку res = InterlockedIncrement(&m_cExtRef); } return res; }

STDMETHODIMP_(DWORD) MyClassObject::ReleaseConnection(DWORD extconn, DWORD, BOOL bLastReleaseKillsStub) { DWORD res = 0; if (extconn & EXTCONN_STRONG) { UnlockModule(); // note external reference // записываем внешнюю ссылку res = InterlockedDecrement(&m_cExtRef); if (res == 0 & bLastReleaseKillsStub) CoDisconnectObject((IExternalConnection*)this, 0); } return res; }

Отметим, что счетчик блокировок модуля будет ненулевым до тех пор, пока существуют неосвобожденные внешние ссылки на объект класса, в то время как внутренние ссылки, удержанные библиотекой COM, игнорируются.

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


Этому положению способствовало присутствие метода LockServer в интерфейсе IClassFactory, который внушает разработчикам мысль, что клиенты будто бы способны в действительности обеспечить выполнение сервера. В то время как большинство разработчиков серверов успешно запирают модуль в методах LockServer, для клиента не существовало надежного способа вызвать данный метод. Рассмотрим следующий клиентский код:

IClassFactory *pcf = 0;

HRESULT hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, О, IID_IClassFactory, (void**)&pcf); if (SUCCEEDED(hr)) hr = pcf->LockServer(TRUE); // keep server running? // поддерживать выполнение сервера?

В первых версиях COM этот фрагмент кода находился бы в условиях серьезной гонки. Отметим, что существует интервал между вызовами CoGetClassObject и IClassFactory::LockServer. В течение этого периода времени другие клиенты могут уничтожить последний остающийся экземпляр класса. Поскольку неосвобожденная ссылка на объект класса игнорируется наивными реализациями серверов, серверный процесс прекратит работу раньше исходного вызова клиентом метода LockServer. Теоретически это можно было бы преодолеть следующим образом:

IClassFactory *pcf = 0; HRESULT hr = S_OK; do { if (pcf) pcf->Release(); hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, 0, IID_IClassFactory, (void**)&pcf); if (FAILED(hr)) break; hr = pcf->LockServer(TRUE); // keep server running? // поддерживать выполнение сервера? } while (FAILED(hr));

Отметим, что данный фрагмент кода периодически пытается подсоединиться к объекту класса и заблокировать его, пока вызов LockServer проходит успешно. Если сервер завершит работу преждевременно — между вызовами CoGetClassObject и LockServer, то вызов LockServer возвратит сообщение об ошибке, извещающее об отсоединенном заместителе, что вызовет повтор последовательности. Под Windows NT 3.51 и в более ранних версиях этот нелепый код был единственным надежным способом получения ссылки на объект класса.

Был признан тот факт, что многие реализации серверов не использовали IExternalConnection для должного управления временем жизни сервера, и в версии COM под Windows NT 4.0 введена следующая модернизация для замены этих наивных реализаций.


При маршалинге ссылки на объект класса в ответ на вызов CoGetClass0bject SCM вызовет метод объекта класса IClassFactory::LockServer. С тех пор как значительное большинство серверов реализуют IClassFactory в своих объектах класса, эта модернизация исполняемых программ COM исправляет значительное количество дефектов. Однако если объект класса не экспортирует интерфейс IClassFactory или если сервер должен выполняться и в более ранних версиях COM, чем Windows NT 4.0, то необходимо использовать технологию IExternalConnection.

Следует обсудить еще одну проблему, относящуюся ко времени жизни сервера. Отметим, что когда сервер решает прекратить работу, то он сообщает о том, что главный поток серверного приложения должен начать свою последовательность операций останова (shutdown sequence) до выхода из процесса. Частью этой последовательности операций останова является вызов CoRevokeClassObject для отмены регистрации его объектов класса. Если, однако, были использованы показанные ранее реализации UnlockModule, то появляются условия серьезной гонки. Возможно, что в промежутке между тем моментом, когда сервер сигнализирует главному потоку посредством вызова SetEvent или PostThreadMessage, и тем моментом, когда сервер аннулирует объекты своего класса, вызывая CoRevokeClassObject, в серверный процесс поступят дополнительные запросы на активацию. Если в этот интервал времени создаются новые объекты, то уже нет способа сообщить главному потоку, что прекращение работы — плохая идея и что у процесса появились новые объекты для обслуживания. Для устранения этих условий гонки в COM предусмотрены две API-функции:

ULONG CoAddRefServerProcess(void); ULONG CoReleaseServerProcess(void);

Эти две подпрограммы управляют счетчиком блокировок модуля от имени вызывающего объекта. Эти подпрограммы временно блокируют любой доступ к библиотеке COM, чтобы гарантировать, что во время установки счетчика блокировок новые активационные запросы не будут обслуживаться. Кроме того, если функция CoReleaseServerProcess обнаружит, что удаляется последняя блокировка в процессе, то она изнутри пометит все объекты класса в процессе как приостановленные и сообщит SCM, что процесс более не является сервером для его CLSID.



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

void LockModule(void) { CoAddRefServerProcess(); // COM maintains lock count // COM устанавливает счетчик блокировок }

void UnlockModule(void) { if (CoReleaseServerProcess() == 0) SetEvent(g_heventShutdown); }

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

Даже при использовании функций CoAddRefServerProcess / CoReleaseServerProcess все еще остаются возможности для гонки. Возможно, что во время выполнения CoReleaseServerProcess на уровне RPC будет получен входящий запрос на активацию от SCM. Если вызов от SCM диспетчеризован после того, как функция CoReleaseServerProcess снимает свою блокировку библиотеки COM, то активационный запрос отметит, что объект класса уже помечен как приостановленный, и в SCM будет возвращено сообщение об ошибке со специфическим кодом (CO_E_SERVER_STOPPING). Когда SCM обнаруживает этот специфический код, он просто запускает новый экземпляр серверного процесса и повторяет запрос, как только новый серверный процесс зарегистрирует себя. Несмотря на системы защиты, используемые библиотекой COM, остается вероятность того, что поступающий активационный запрос будет выполняться одновременно с заключительным вызовом функции CoReleaseServerProcess. Чтобы избежать этого, сервер может явно возвратить CO_E_SERVER_STOPPING как из IClassFactory::Create Instance, так и из IPersistFile::Load в том случае, если он определит, что по окончании запроса на прекращение работы был сделан еще какой-то запрос. Следующий код демонстрирует этот способ:

STDMETHODIMP MyClassObject::CreateInstance(IUnknown *puo, REFIID riid, void **ppv) { LockModule(); // ensure we don't shut down while in call // убеждаемся в том, что не прекращаем работу // во время вызова HRESULT hr; *ppv = 0; // shutdown initiated? // процесс останова запущен? DWORD dw = WaitForSingleObject(g_heventShutdown, 0); if (dw == WAIT_OBJECT_0) hr = CO_E_SERVER_STOPPING; else { // normal CreateInstance implementation // нормальная реализация CreateInstance } UnlockModule(); return hr; }

Во время написания этого текста ни одна из коммерческих библиотек классов COM не реализовывала этот способ.


Содержание раздела