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

2bbc099f

Стандартный маршалинг, потоки и протоколы


Подробности того. как СОМ на самом деле преобразует запросы ORPC в потоки, нигде не документированы и подлежат изменениям но мере развития реализации библиотеки СОМ. Описания, содержащиеся в данном разделе, относятся только ко времени написания этого текста, но, конечно, некоторые детали реализации могут измениться в последующих выпусках СОМ.

Когда в процессе инициализируется первый апартамент, СОМ включает RPC-слой времени выполнения, переводя процесс в RPC-сервер. Если апартамент имеет тип МТА или RTA, то используется RPC-протокол ncalrpc, который является оберткой вокруг портов LPC (local procedure call - локальный вызов процедуры) под Windows NT. Если тип апартамента - STA, то используется последовательность закрытого протокола, основанная на очередях сообщений MSG Windows. При первом обращении внехостовых клиентов к объектам, постоянно находящимся в процессе, в процессе регистрируются дополнительные сетевые протоколы. Когда процесс впервые начинает использовать протоколы, отличные от протокола MSG Windows, запускается кэш потоков RPC. Этот кэш потоков начинается с одного потока, который ожидает приходящих запросов на установление соединения, запросов RPC или других действий, специфических для протоколов. Как только произойдет любое из этих событий, кэш потоков RPC переключит поток на обслуживание запроса и будет ждать следующих действий. Для предотвращения излишних затрат на создание/уничтожение потоков эти потоки возвращаются в потоковый кэш, где они будут ждать дальнейшей работы. Если работы нет, то потоки самоуничтожатся по истечении заранее определенного периода бездействия. Теневая сторона этого заключается в том, что кэш потоков RPC растет или сжимается в зависимости от занятости объектов, экспортированных из апартаментов процессов. С точки зрения программирования важно заметить, что кэш потоков RPC динамически размещает потоки, основанные на ORPC-запросах, приходящие по любым протоколам, кроме протокола Windows MSG, который будет обсуждаться позднее в этом разделе.


Когда поступающий ORPC- запрос переадресуется потоку из кэша. поток выделяет из заголовка ORPC-вызова IPID (идентификатор указателя интерфейса) и находит соответствующий администратор заглушек и интерфейсную заглушку. Поток определяет тип того апартамента, в котором хранится объект, и если объект находится в апартаменте типа МТА или RTA, поток входит в апартамент объекта и вызывает метод IRpcStubBuffer::Invoke на интерфейсную заглушку. Если апартамент имеет тип RTA, то в течение вызова метода последующие потоки не будут допускаться к объекту. Если апартамент имеет тип МТА, то последующие потоки могут обращаться к объекту одновременно. В случае внутрипроцессных RTA/MTA-связей канал может сократить кэш потоков RPC и использовать поток клиента повторно, временно входя в апартамент объекта. Если бы МТА и RTA были единственными типами апартаментов, то этого было бы достаточно.



Диспетчеризация вызовов в STA более сложна, так как в существующий STA не могут войти никакие другие потоки. К сожалению, когда ORPC-запросы поступают от внехостовых клиентов, они координируются с использованием потоков из RPC кэша потоков, которые по определению не могут выполняться в STA объекта. Для того чтобы войти в STA и направить вызов потока STA, RPC-поток использует АРI-функцию PostMessage, которая ставит сообщение в специальную MSG-очередь сообщений STA-потоков, как показано на рис. 5.5. Эта очередь представляет собой ту же очередь FIFO (first-in, first-out), которую применяет оконная система. Это означает, что для завершения диспетчеризации вызова STA-поток должен обслуживать очередь с помощью одной из вариаций следующего кода:

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

Этот код означает, что STA-поток имеет по меньшей мере одно окно, которое может принимать сообщения. Когда поток входит в новый STA посредством вызова CoInitializeEx, СОМ создает новое невидимое окно, вызывая CreateWindowEx. Это окно связано с зарегистрированным в СОМ оконным классом, элемент которого WndProc ищет определенные заранее оконные сообщения и обслуживает соответствующий ORPC-запрос посредством вызова метода IRpcStubBuffer::Invoke на интерфейсную заглушку.


Отметим, что поскольку окна, подобно объектам на основе STA, обладают потоковой привязкой, WndProc будет выполняться в апартаменте объекта. Чтобы избежать чрезмерного количества переключения потоков, в версии СОМ для Windows 95 предусмотрен транспортный RPC-механизм, который обходит RPC-кэш потоков и вызывает PostMessage из потока вызывающего объекта. Этот перенос возможен только в том случае, если клиент находится на том же хосте, что и объект, поскольку API-функция PostMessage не работает в сети.

Для предотвращения взаимоблокировки все типы апартаментов СОМ поддерживают реентерабельность (повторную входимость). Когда поток в апартаменте делает запрос на объект вне апартамента вызывающего объекта посредством заместителя, то могут обслуживаться и другие поступающие запросы методов, пока поток вызывающего объекта находится в ожидании ORPC-ответа па первый запрос. Без этой поддержки было бы невозможно создавать системы, основанные на совместно работающих объектах. При написании следующего кода предполагалось, что CLSID_Callback является внутрипроцессным сервером, поддерживающим модель вызывающего потока, и что CLSID_Object является классом, сконфигурированным для активации на удаленной машине:

ICallback *pcb = 0; HRESULT hr = CoCreateInstance(CLSID_Callback, 0, CLSCTX_ALL, IID_ICallback, (void**)&pcb); assert(SUCCEEDED(hr)); // callback object lives in this apt. // объект обратного вызова живет в этом апартаменте I0bject "po = 0; hr = CoCreateInstance(CLSID_Object, 0, CLSCTX_REMOTE_SERVER, IID_Iobject, (void **)&po); assert(SUCCEEDED(hr)); // object lives in different apt. // объект живет в другом апартаменте // make a call to remote object, marshaling a reference to // the callback object as an [in] parameter // делаем вызов удаленного объекта, маршалируя ссылку // на объект обратного вызова как на [in]-параметр hr = po->UseCallback(pcb); // clean up resources // очищаем ресурсы pcb->Release(); pco->Release();



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



STDMETHODIMP Object::UseCallback(ICallback *pcb) { HRESULT hr = pcb->GetBackToCallersApartment(); assert(SUCCEEDED(hr)); return S_OK;



Напомним, что когда [in]- параметр передается через метод заместителя UseCallback, то заместитель вызывает CoMarshalInterface для маршалинга интерфейсного указателя ICallback. Поскольку указатель ссылается на объект, находящийся в апартаменте вызывающего объекта, то этот апартамент становится экспортером объектов и поэтому любые межапартаментные вызовы объекта должны обслуживаться в апартаменте вызывающего объекта. Когда заглушка интерфейса IObject демаршалирует интерфейс ICallback, она создает заместитель для передачи его реализации метода UseCallback. Этот заместитель представляет объект при промежуточном соединении с объектом обратного вызова, которое продолжается на протяжении всего времени вызова. Время существования этого заместителя/соединения может превысить время вызова, если реализация метода просто вызовет AddRef на заместитель:

STDMETHODIMP Object::UseCallback(ICallback *pcb) { if (!pcb) return E_INVALIDARG; // hold onto proxy for later use // сохраняем в заместителе для дальнейшего использования (m_pcbMyCaller = pcb)->AddRef(); return S_OK; }

Обратное соединение с апартаментом клиента будет длиться до тех пор, пока заместитель не будет освобожден объектом. Поскольку все апартаменты СОМ могут получать ORPC-запросы, объект может послать обратный вызов в апартамент клиента в любое время.

Реентерабельность реализуется для каждого типа апартаментов по-разному. Наиболее проста реализация в случае МТА, так как МТА-апартаменты не гарантируют параллелизма и не указывают, какой поток будет обслуживать заданный вызов метода. Повторный вызов может прийти в то время, когда МТА-поток заблокирован в канале в ожидании ORPC-ответа. Тогда RPC-поток, получающий повторный запрос, просто входит в МТА и обслуживает вызов своими ресурсами. Тот факт, что другой поток апартамента заблокирован и ожидании ORPC-ответа, не влияет на диспетчеризацию вызова.


В случае реализации RTA — когда поток, выполняющийся в RTA, делает межапартаментный вызов посредством заместителя, — канал уступает контроль над апартаментом, снимая блокировку всего RTA и разрешая тем самым обработку поступивших вызовов. И снова, но причине отсутствия привязки объектов к потокам в RTA, RPC-поток, получающий ORPC-запрос, может просто войти в апартамент RTA и обслужить вызов сразу после блокирования всего RTA.

Реализация реентерабельности для апартаментов STA более сложна. Поскольку STA-объекты обладают привязкой к потоку, то когда поток делает межапартаментный вызов из STA, СОМ не может разрешить потоку сделать блокирующий вызов, который предотвратил бы обработку входящих ORPC-запросов. Когда поток вызывающего объекта входит в метод канала SendReceive, чтобы послать ORPC-запрос и получить ORPC-ответ, этот канал захватывает поток вызывающего объекта и помещает его во внутренний оконный MSG-цикл. Это аналогично тому, что происходит при создании потоком модальных диалоговых окон. В обоих случаях поток вызывающего объекта вынужден обслуживать определенные классы оконных сообщений во время выполнения этой операции. В случае модальных диалоговых окон поток должен обслуживать основные оконные сообщения, чтобы разморозить основной пользовательский интерфейс. В случае межапартаментного вызова метода в СОМ поток должен обслуживать не только обычные оконные сообщения пользовательского интерфейса, но и оконные сообщения, относящиеся к поступающим ORPC-запросам. По умолчанию канал будет разрешать обслуживание всех поступающих ORPC-вызовов, пока клиентский поток ожидает ORPC-ответа. Такой режим можно настроить с помощью установки в потоке специального фильтра сообщений.

Фильтры сообщений являются уникальными для STA. Фильтр сообщений — это объект СОМ для каждого STA, который используется для решения вопроса, организовать диспетчеризацию поступающих ORPC-запросов или нет. Кроме того, фильтры сообщений используются для размещения задержанных сообщений пользовательского интерфейса, пока поток STA ожидает ORPC-ответа внутри канала.


Фильтры сообщений выставляют интерфейс IMessageFilter:

[ uuid(00000016-0000-0000-C000-000000000046),local, object ] interface IMessageFilter : IUnknown { typedef struct tagINTERFACEINFO { IUnknown *pUnk; // which object? // чей объект? IID iid; // which interface? // чей интерфейс? WORD wMethod; // which method? // чей метод? } INTERFACEINFO;

// called when an incoming ORPC request arrives in an STA // вызывается, когда входящий ORPC-запрос поступает в STA DWORD HandleInComingCall( [in] DWORD dwCallType, [in] HTA5K dwThreadIdCaller, [in] DWORD dwTickCount, [in] INTERFACEINFO *pInterfaceInfo );

// called when another STA rejects or postpones an ORPC request // вызывается, когда другой STA отклоняет или откладывает ORPC-запрос

DWORD RetryRejectedCall( [in] HTASK dwThreadIdCallee, [in] DWORD dwTickCount, [in] DWORD dwRejectType );

// called when a non-COM MSG arrives while the thread is // awaiting an ORPC response // вызывается, когда поступает не СОМ'овское MSG, пока // поток ожидает ORPC-ответа DWORD MessagePending( [in] HTASK dwThreadIdCallee, [in] DWORD dwTickCount, [in] DWORD dwPendingType ); }

Для установки специального фильтра сообщений в СОМ существует API-функция CoRegisterMessageFilter:

HRESULT CoRegisterMessageFilter([in] IMessageFilter *pmfNew, [out] IMessageFilter **ppmfOld);

CoRegisterMessageFilter связывает указанный фильтр сообщений с текущим STA. Предыдущий фильтр сообщений возвращается для того, чтобы вызывающий объект мог восстановить его в дальнейшем.

Когда бы входящий ORPC-запрос ни пришел в STA-поток, вызывается метод фильтра сообщений HandleIncomingCall, который дает апартаменту возможность принять, отклонить или отложить вызов. HandleIncomingCall используется как реентерабельными, так и нереентерабельными вызовами. Параметр dwCallType показывает, какой тип вызова был получен:

typedef enum tagCALLTYPE { CALLTYPE_TOPLEVEL, // STA not in outbound call // STA не в исходящем вызове CALLTYPE_NESTED, // callback on behalf of outbound call // обратный вызов от имени исходящего вызова CALLTYPE_ASYNC, // asynchronous call // асинхронный вызов CALLTYPE_TOPLEVEL_CALLPENDING, // new call while waiting // новый вызов во время ожидания CALLTYPE_ASYNC_CALLPENDING // async call while waiting // асинхронный вызов во время ожидания } CALLTYPE;



Вложенный (реентерабельный) вызов и незаконченный (нереентерабельный) вызов верхнего уровня происходят, пока поток ожидает ORPC-ответа в канале. Вызовы верхнего уровня происходят в тех случаях, когда в апартаменте нет активных вызовом.

В СОМ определено перечисление, которое должна возвратить реализация HandleIncomingCall, чтобы указать, что произошло с вызовом:

typedef enum tagSERVERCALL { SERVERCALL_ISHANDLED, // accept call and forward to stub // принимаем вызов и направляем его заглушке SERVERCALL_REJECTED, // tell caller that call is rejected // сообщаем вызывающему объекту, что вызов отклонен SERVERCALL RETRYLATER // tell caller that call is postponed // сообщаем вызывающему объекту, что вызов отложен } SERVERCALL;

Если функция HandleIncomingCall фильтра сообщений возвращает SERVERCALL_ISHANDLED, то вызов будет направлен в интерфейсную заглушку для демаршалинга. Фильтр сообщений, принятый по умолчанию, всегда возвращает SERVERCALL_ISHANDLED. Если HandleIncomingCall возвращает SERVERCALL_REJECTED или SERVERCALL_RETRYLATER, то фильтр сообщений вызывающего объекта будет информирован о положении вызова и ORPC-запрос будет отклонен.

Когда фильтр сообщений отвергает или откладывает вызов, то фильтр сообщений вызывающего объекта информируется об этом с помощью метода RetryRejectedCall. Этот вызов происходит в контексте апартамента вызывающего объекта, и реализация метода RetryRejectedCall фильтра сообщений может решать, повторять ли отложенный вызов. Параметр dwRejectType указывает, был ли вызов отклонен или отложен. Реализация канала вызывающего объекта будет решать, какое действие предпринять, в зависимости от значения, возвращенного RetryRejectedCall. Если RetryRejectedCall возвращает -1, то канал предположит, что повторных попыток не требуется, и немедленно заставит заместитель возвратить HRESULT, равный RPC_E_CALL_REJECTED. По умолчанию фильтр сообщений всегда возвращает -1. Любое другое значение, возвращаемое методом RetryRejectedCall, интерпретируется как число миллисекунд, через которое следует повторить вызов.


Поскольку это согласование осуществляется внутри канала, то не требуется повторного ORPC-запроса со стороны заместителя. В сущности, интерфейсные маршалеры не имеют ни малейшего понятия о процессах в фильтре сообщений.

Когда размещенный в STA поток блокирован в канале в ожидании ORPC-ответа, то не связанные с СОМ оконные сообщения могут поступать в MSG-очередь потока. Когда это происходит, то фильтр сообщений STA уведомляется об этом посредством метода MessagePending. Фильтр сообщений, принятый по умолчанию, разрешает диспетчеризацию некоторых оконных сообщений, чтобы предотвратить замораживание всей оконной системы. Тем не менее, действия ввода (например, щелчки мышью, нажатие клавиш) не учитываются, чтобы конечный пользователь не начал новое взаимодействие с системой. Как уже отмечалось ранее, фильтры сообщений существуют только в апартаментах STA и не поддерживаются в RTA или МТА. Фильтры сообщений лишь обеспечивают лучшую интеграцию СОМ с потоками, обрабатывающими события от пользовательского интерфейса. Из этого следует, что все эти потоки должны выполняться в однопотоковых апартаментах. Большинство потоков, обрабатывающих события от пользовательского интерфейса, захотят установить специальный фильтр сообщений, чтобы убедиться в том, что входящие запросы не обслуживаются, пока приложение находится в такой критической фазе, в которой реентерабельность может привести к семантическим ошибкам. Фильтры сообщений не следует применять в качестве универсального механизма для управления потоками. Реализация фильтров сообщений печально известна своей неэффективностью в тех случаях, когда вызовы отклоняются или откладываются. Это делает фильтры сообщений малоприспособленными в качестве механизма для управления потоками в высокопроизводительных приложениях.

1

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

2

По недоразумению широко распространено мнение, что для обеспечения двухсторонней связи или обратных вызовов требуются точки стыковки (Connection Points). Как описывается в главе 7, точки стыковки необходимы только для поддержки программ обработки событий в Visual Basic и для сред подготовки сценариев.


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