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

COM - потоки и контексты

Автор: Чистяков В.Ю.
Источник: «Технология Клиент-Сервер»

Версия текста: 1.0.2
COM своими глазами
Прежде, чем начать
Первое клиентское приложение
Клиентское приложение 2 – входим в многопоточный мир
IMessageFilter – блокировка GUI при COM-вызовах
Когда создать свой обработчик?
Завершение работы приложений при WM_QUERYENDSESSION и WM_ENDSESSION
IMessageFilter::MessagePending
Альтернативный подход
W2k и COM
Менеджер каталогов
Основы перехвата
Перехват и контексты
Относительность контекстов
Контексты и апартаменты
Контексты и активности
Вывод

Код к статье.

При реализации модели апартаментов (apartment model) программисты из Microsoft воспользовались своими «окнами». Вернее, не своими окнами, а их очередями сообщений. В оконных очередях сообщений реализован гибкий и производительный механизм обработки поступающих сообщений. Поддерживается как синхронная, так и асинхронная обработка. Rama Krishna предложил неординарный, но очень простой способ показать, как работает апартаментная модель в COM. Суть этого метода заключается в том, чтобы подглядывая за оконными очередями (создаваемыми и используемыми COM) воочию увидеть как работает COM.

COM своими глазами

К сожалению, увидеть окна, которые создает COM, можно только в ОС, вышедших до появления Windows 2000 (W2k). Это связано с тем, что в W2k появились новый тип окон - Message-Only-окна. Такие окна не отображаются утилитой Spy++ (о ней речь пойдет позже). Собственно, Message-Only-окна и есть облегченный вариант окна, имеющий только очередь сообщений и не имеющий ненужных в этом случае графических наворотов. Подводя итог вышесказанному, можно сказать, что этот пример бесполезно пытаться выполнять под управлением W2k, так как вы попросту не увидите окон, которые создает подсистема COM. Несмотря на невидимость окон, пример будет успешно функционировать в W2k, но лучше найти компьютер с установленной Windows NT (NT) 4.0 или Windows 95 и DCOM.

Прежде, чем начать

Для нашего эксперимента нам потребуются:

  1. Visual C++ Version 5.0 или 6.0 (желательно 6.0)
  2. OLE/COM Object Viewer (входит в состав MS Visual Studio (VS), MS Visual C++ (VC) и MS Platform SDK.
  3. Spy++ – Window Spying Utility, поставляемая с VS и VC.

Прежде, чем двинуться дальше, распакуйте проекты. Разверните файл Comthreading.zip и откройте workspace "COMThreading.dsw" в VC++. Проекты объединены в workspace «COMThreading». Ниже приводится список проектов, входящих в workspace:

Client

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

Server

COM DLL-сервер, реализованный на ATL. Он содержит один объект, поддерживающий единственный простой (не дуальный) интерфейс IServer. Этот интерфейс имеет единственный метод SimpleMethod выводящий окно с сообщением.

ServerPS

Простой Win32DLL- проект предназначенный для создания proxy-stub DLL из файлов, генерированных компилятором MIDL.

Для начала активизируйте проект Server и скомпилируйте его. В результате будет создана и зарегистрирована Server.dll.

Откройте «OLE/COM Object Viewer» (например, из меню Tools в VC) и посмотрите, что мы теперь имеем.

Проверьте в меню «View OLE/COM Object Viewer’а», включен ли режим «Expert Mode». В ветке «Object Classes» раскройте подветку «All Objects». Найдите в ней подветку «ServerApp Class». Это и есть класс нашего серверного объекта. После этих операций окно «OLE/COM Object Viewer»'а должно выглядеть примерно так, как на рисунке 1. Заметьте, что он показывает только интерфейс IUnknown и никакого IServer. Это потому, что IServer не был распознан в Реестре.


Рис. 1

Первое клиентское приложение

Теперь пора создать клиентское приложение. Чтобы упростить задачу, используем поддержку COM компилятора. С помощью smart pointer'ов, создаваемых компилятором при обработке директивы «#import...» удобнее создать объект ServeApp и вызвать его метод – SimpleMethod. Директива «#import» имеет ряд ключей позволяющих генерировать описание разной степени навороченности. По умолчанию компилятор создает оболочки над каждым объектом, интерфейсом, методом и т.п. Эти оболочки используют структурную модель обработки ошибок C++, избавляя от необходимости напрямую работать с HRESULT-значениями. Они также задают значения по умолчанию параметров методов.

Вот код клиентского приложения.

#include <windows.h>
#pragma hdrstop

#import "Server.tlb" no_namespace
#include "MyComutl.h"

int main(int , char** )
{
   CComInit cominit(COINIT_APARTMENTTHREADED);
   IServerPtr spServer(__uuidof(ServerApp));
   spServer->SimpleMethod();
   return 0;
}

CComInit – класс, производящий инициализацию COM посредством вызова CoInitializeEx в своем конструкторе, и деинициализирующий COM вызовом CoUninitialize в своем деструкторе.

В функции main деструктор IServerPtr (smart pointer, который «держит» указатель на интерфейс серверного объекта и освобождает его при уничтожении, в нашем случае, переменной spServer) вызывается до деструктора CComInit. Как вы понимаете, объявление «IServerPtr spServer(__uuid­of(ServerApp));» создает экземпляр объекта ServerApp, запрашивает у него указатель на интерфейс IServer и сохраняет этот указатель внутри smart pointer'а – spServer. В конце этого нехитрого кода вызывается метод SimpleMethod.

Активизируйте проект «Client», скомпилируйте его, поместите точку прерывания на первую строку (объявление переменной cominit) и запустите проект на выполнение (F5).

Когда выполнение остановится на точке прерывания, откройте Spy++ (например, из меню Tools VC) и в меню Spy выберите пункт Processes. Найдите и раскройте ветку «Client» в дереве процессов (если Spy++ был открыт до этого, и в нем уже было открыто окно Processes, необходимо выбрать его и нажать F5, чтобы обновить информацию). Окно Spy должно выглядеть примерно так, как изображено на рисунке 2.


Рис. 2

Обратите внимание, что Spy++ опознал процесс Client. Дочерний элемент ‘XXXXXX D:\xxxxxxx\ComThread­ing\Debug\Client.exe ConsoleWindowClass’, это дескриптор окна, заголовок и имя класса окна консоли нашего клиентского приложения.

Теперь продвинемся в отладчике на строку вперед (F10). После этого вернемся в Spy++ и обновим вид нажатием F5. Теперь дочерние элементы ветки Client выглядят так:


Рис. 3

Заметьте, внутри вызова CoInitializeEx(NULL, COINIT_APA­RT­MENTTHREADED) для потока в котором происходил этот вызов было создано новое (скрытое) окно с именем «OleMainThreadWndName» (не забудьте, под W2k этого окна видно не будет!). Класс этого окна «OleMainThre­ad­Wnd­Class». Если поток вызывает CoInitialize(NULL) или CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), то создается так называемый Single-Threaded Apartment (однопоточный апартамент), или сокращенно STA. Если же поток вызывает CoInitializeEx(NULL, COINIT_MULTI­THREADED), то он (поток) помещается в так называемый Multi-Threaded Apartment (многопоточный апартамент), или сокращенно MTA.

В одном процессе может существовать сколько угодно STA и только один MTA. Для каждого апартамента (будь то MTA или STA COM создает скрытое окно) это окно (вернее его очередь сообщений) нужно для синхронизации вызовов производимых к апартаменту. Один поток физически не может вызвать другой. Выполнение любого кода в потоке (в том числе и вызовы функций) производится именно в этом потоке, независимо от того, в какой области памяти процесса код располагается, и кем и как он был загружен. Совершенно невозможно из одного потока вызвать функцию из другого потока. Апартаментная модель предполагает, что вызовы к объектам (то есть к их коду) должны производиться из одного из потоков, входящих в этот апартамент. Оконная очередь сообщений нужна как раз для того, чтобы переключаться между потоком, инициирующим вызов, и потоком, закрепленным за апартаментом (Напомню что для STA это единственный поток, а для MTA – любой поток, прикрепленный к MTA.). Такое переключение происходит путем преобразования вызова в оконное сообщение, помещаемое в очередь этого самого скрытого окна. Этим занимается proxy объекта. То есть каждый поток, который хочет вызывать методы у компонента, не входящего в его апартамент, должен иметь указатель не на объект, а на его proxy. Поток, который первым вызывает CoInitialize(NULL) или CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) становится главным STA (Main STA), что отражается в названии созданного окна («OleMainThreadWndName»).

Далее можете или прервать выполнение (Shift+F5) или нормально завершить выполнение, нажав F5...

Клиентское приложение 2 – входим в многопоточный мир

В простом DLL-сервере указатели на интерфейсы могут быть переданы разным потокам, но если DLL создавалась без расчета на многопоточную работу, сбои неизбежны. В некоторых случаях это может привести к трудноуловимым ошибкам, поскольку сбои происходят в непредсказуемые моменты. Как COM-интерфейс, полученный от объекта, созданного в потоке одного апартамента, может быть безопасно использован потоком, входящим в другой апартамент из того же процесса? Ответ COM – «с помощью маршалинга интерфейса из одного потока в другой». Хотя маршалинг привычно (и справедливо) ассоциируется с RPC, в нашем случае маршалинг имеет нечто общее с обеспечением потокобезопасности. Я специально выражаюсь так нечетко – ясность наступит, когда мы разберемся, что происходит на практике.

Чем же именно занимается маршалинг и как это обеспечивает потокобезопасность? Сейчас разберемся...

Чтобы маршалинг работал, нам нужно создать и зарегистрировать proxy/stub dll. Так что скомпилируйте проект ServerPS. При этом автоматически зарегистрируется proxy/stub dll. Чтобы убедиться, что она была зарегистрирована, переключитесь еще раз в «OLE/COM Object Viewer» и обновите информацию о компоненте (например, выберите пункт контекстного меню «Release Instance» и снова откройте этот пункт). Заметьте, что «OLE/COM Object Viewer» показывает интерфейс IServer (см. рисунок 4). Заметьте так же, что описание интерфейса IServer содержит пункт ProxyStubClsid32, значение которого указывает на ключ, приведенный ниже. Это ключ, описывающий Proxy/Stub DLL. Он должен указывать на нашу библиотеку ServerPS. Подробнее о маршалинге и интерфейсов и конкретно о Proxy/Stub можно прочитать в прошлом номере нашего журнала.


Рис. 4

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

Теперь нужно добавить в клиентское приложение некий код для создания нескольких потоков. Этот код находится в файлах mycomutl.cpp и client1.cpp. Все, что нужно – скопировать код из client1.cpp и вставить его в client.cpp, или удалить из проекта client.cpp и добавить client1.cpp – как вам удобнее.

Каждый поток в процессе, нуждающийся в вызовах функций COM должен инициализировать COM, используя CoInitialize(Ex) и деинициализировать его до окончания работы. Для этого нами будет использоваться простая helper-функция BeginCOMThread (ее код находится в mycomutl.cpp):

namespace
{
   struct COMThreadingHelper
   {
      DWORD dwTM; //Потоковая модель (Threading model)
      THREADFN pfnStart;
   };

   unsigned int _stdcall ThreadProc(LPVOID pParam)
   {
      COMThreadingHelper* pCT = reinterpret_cast(pParam);
      
      //Инициализируем COM для этого потока
      CComInit cominit(pCT->dwTM);
      
      //Вызываем пользовательскую функцию 
      (pCT->pfnStart)();

      delete pCT;
      
      return 0;
   }
}
HANDLE BeginCOMThread(DWORD dwTM, THREADFN pfnStart)
{
   unsigned int dwId;
   
   COMThreadingHelper* pCT = new COMThreadingHelper;

   pCT->dwTM = dwTM;
   pCT->pfnStart = pfnStart;
   
   return (HANDLE) _beginthreadex(
                  NULL,         //Атрибут защиты
                  0,            //Размер стека
                  ThreadProc,   //Адрес главной процедуры потока
                  (void*)(pCT), //Параметр передающийся в ThreadProc
                  0,            //0 – Запускать сразу после создания
                  &dwId         //[out] идентификатор потока
                  );
}

Определение COMThreadingHelper и ThreadProc находятся в безымянном namespace. Этот способ применяется в С++, чтобы сделать функции и структуры локальными.

Стоит упомянуть, что вместо Win32-функции CreateThread используется функция из С-runtime библиотеки _beginthreadex. Причина этого в том, что C-runtime поддерживает специфичные для потока глобальные данные, например, errno и т.д. Если вы намерены использовать функции из С-runtime, то потоки необходимо создавать именно таким образом, если нет (например, вы пользуетесь только Win32-API и ATL), можно использовать функцию CreateThread.

Итак, у нас есть функция, создающая поток и инициализирующая COM для него. Для того чтобы произвести маршалинг указателя на интерфейс IServer из одного потока в другой, мы воспользуемся одной из самых длинных (в смысле имени) функций OLE32 API CoMarshalInterThreadInterfaceinStream. Теперь функция main выглядит так:

int main(int , char** )
{
   CComInit cominit(COINIT_APARTMENTTHREADED);

   IServerPtr spServer(__uuidof(ServerApp));
   spServer->SimpleMethod();

   //маршалим (вручную!) указатель на интерфейс IServer
   //находящийся в spServer
   HRESULT hr = CoMarshalInterThreadInterfaceInStream(
      __uuidof(IServer), //IID интерфейс, подвергающегося маршалингу
      spServer, //Указатель на интерфейс
      &g_pStm   //Указатель на IStream куда помещается бинарное описание 
                //интерфейса
   );
   
   //Запускам STA-поток
   HANDLE hThread = BeginCOMThread(COINIT_APARTMENTTHREADED, AThread);
   
   //Ожидание окончания выполнения потока (Дескриптор потока переходит в 
   //сигналящее состояние когда поток завершает свое состояние
   WaitForSingleObject(hThread, INFINITE);
   
   //_endthreadex не закрывает дескриптора в отличие от _endthread 
   //Когда исполнение функции, вызванной из _beginthreadex,
   //завершается,_endthreadex вызывается автоматически. Тем не менее нам нужно
   //закрыть дескриптор потока...
   CloseHandle(hThread);

   return 0;
}

Глобальная переменная g_pStm типа «указатель на IStream» используется функциями COM для маршалинга интерфейса между потоками.

Чтобы основное приложение не закончило работу раньше созданного нами потока, нам следует использовать функцию WaitForSingleObject.

Функция AThread, получает (с помощью функции CoGetInterfaceAndReleaseStream) указатель на интерфейс IServer и производит вызов метода IServer::SimpleMethod у объекта созданного в основном потоке приложения. Вот ее код:

//Эта функция выполняется в отдельном потоке
void AThread()
{
   IServerPtr spServer;
   HRESULT hr = CoGetInterfaceAndReleaseStream(g_pStm, 
                           __uuidof(IServer), 
                           reinterpret_cast<LPVOID*>(&spServer));
   
   spServer->SimpleMethod();
}

Поставьте точку прерывания в начале функции AThread. Запустите отладчик. Как только выполнение программы остановится на этой точке, откройте Spy++ и еще раз посмотрите на клиентский процесс (если Spy++, то не забудьте обновить информацию). На этот раз там должно быть нечто такое:


Рис.5.

Как вы видите Spy++ показывает оба потока.

Пройдите в пошаговом режиме до вызова SimpleMethod. Заметьте, что после вызова CoGetInterfaceAndReleaseStream значение указателя spServer установлено в некоторое значение. Продвиньтесь еще на одну строку. Что происходит? Приложение зависло. Все, что можно сделать, это выбрать «stop debugging» из меню Debug (или нажать Shift+F5).

Если бы вызов spServer->SimpleMethod был прямым (без маршалинга) вызовом функции из dll, приложение не повисло бы. Но мы провели маршалинг интерфейса в другой поток и теперь используем его в другом потоке. Вызов spServer->SimpleMethod – уже не прямой вызов, он должен пройти системный код, прежде чем достигнет реальной функции SimpleMethod в Server.dll.

Поскольку указатель на интерфейс, полученный в результате маршалинга, на самом деле соответствует объекту, созданному в другом потоке, а объект не может работать с несколькими потоками, функция SimpleMethod объекта должна вызываться из потока, где создавался объект. Как уже говорилось ранее, вызывающий поток должен как-то сообщить потоку из апартамента, в котором был создан объект, что нужно вызвать его функцию SimpleMethod. Контекст исполнения следует изменить. Как это сделать? Это несложно, если вспомнить скрытое окно, созданное потоком, ассоциированным с объектом. Все, что нужно вызывающему потоку – так это послать сообщение в скрытое окно с помощью SendMessage. Как только оконное сообщение приходит по назначению, поток, которому принадлежит объект, прочтет это сообщение и преобразует данные, присланные с этим сообщением, в стековый фрейм и вызовет необходимый метод. Заметьте, вызов метода будет осуществлен из потока, в котором объект и был создан, то есть, в нашем случае, из главного потока приложения. Но мы не реализовали никаких циклов обработки сообщений в основном потоке приложения, вместо этого мы заморозили поток, переведя его в режим ожидания завершения исполнения другого потока. Это тупик.

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

Нам нужно изменить основную функцию, включив в нее диспетчеризацию сообщений. Функцию WaitForSingleObject нужно заменить следующим кодом (можно взять его в Client2.cpp или просто подключить Client2.cpp к проекту вместо Client1.cpp):

//Ждем окончания работы потока

   //Дескриптор потока переходит в сигналящее состояние если потока завершил
   //свою работу
   for(;;)
   {
      DWORD dwRet = MsgWaitForMultipleObjects(
          1,        //Счетчик объектов ядра (в нашем случае он один)
          &hThread, //Указатель на первый элемент массив объектов ядра
         FALSE,     //Ждать все объекты Wait (в нашем случае он один)
         INFINITE,  //Сколько ждать? (в нашем случае до посинения)
         QS_ALLINPUT//Какие сообщения ждать (мы ждем все сообщения)
      );
      
      //Если поток закончил свою работу...
      if(dwRet != WAIT_OBJECT_0 + 1)
         break; //то выходим из цикла

      //Удалить сообщения из очереди
      MSG msg;
      while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0)
      {
         //Несущественно
         TranslateMessage(&msg);
         //Доставить сообщение по назначению
         DispatchMessage(&msg);
      }
   }
   ...

Поместив точку прерывания в функцию AThread, снова запустите программу в отладчике. Когда выполнение прервется, откройте Spy++ и раскройте ветку «Client». Она будет выглядеть практически так же, как и в предыдущий раз. Теперь поставьте точку прерывания в методе CServerApp::Simple­Method. Он находится в файле ServerApp.cpp проекта Server и позвольте отладчику продолжить исполнение. Когда выполнение остановится на этой точке, посмотрите на стек вызовов. Стек вызовов будет выглядеть примерно так:


Рис.6

Как следует из стека вызовов, в эту точку мы попали через методы Kernel32.dll, rpcrt4.dll и ole32.dll, а не напрямую из клиентского приложения. Это и есть работа маршалинга. Вызов метода даже для in-proc сервера должен идти через код системного уровня. Все это делается для обеспечения потокобезопасности методов компонента.

Теперь, оставив точку прерывания на прежнем месте в функции AThread, перезапустите приложение в отладчике. После CoGetInterfaceAndReleaseStream снова откройте Spy++. На этот раз вы увидите нечто типа:

Разница в том, что теперь и основной поток, и созданный нам содержат скрытые окна. Во втором потоке имеется скрытое окно того же класса, но с другим названием – «OLEChannelWnd». Будет полезно проверить, какие сообщения посылаются каждому из окон. Spy++ снова поможет нам, щелкните правой кнопкой мыши по каждому из окон и выберите «messages» из контекстного меню. Теперь все сообщения, поступающие в эти окна, будут показаны в Spy++.


Рис. 7

Позволим приложению завершить работу. Теперь вернемся в Spy++ и посмотрим на лог сообщений:


Рис. 8

Обратите внимание на два WM_USER-сообщения. В wParam этих сообщений можно мгновенно узнать дескриптор другого потока. Итак, становится ясным, что COM посылает сообщения WM_USER скрытому окну, ассоциированному с потоком, к которому принадлежит объект, в случае, если метод объекта вызывается из другого потока через «отмаршаленный» интерфейс. Все это делает код маршалинга, находящийся в основном в rpcrt4.dll.

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

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

В W2k для этого введена новая потоковая модель – Neutral, а в предыдущих версиях Windows можно пользоваться специальным объектом FreeThreaded Marshaler (FTM) помечая компонент как поддерживающий модель Both (то есть Apartment и Free одновременно). Both позволяет объекту создаваться в любом (Apartment или Free) апартаменте, а агрегация FTM – избегать создания межпоточного proxy. Недостатков у объектов, агрегирующих FTM (далее – FTM-объектов), два. Первый – такие объекты не могут принадлежать к «COM+»-контексту (о нем речь пойдет далее), второй – это то, что такой объект не может хранить указатель на интерфейсы других объектов, если только эти объекты тоже не агрегируют FTM. Но первый недостаток может быть и достоинством. Дело в том, что при попытке запросить «COM+»-контекст будет возвращен контекст апартамента, из которого был вызван метод FTM объекта. Это дает возможность создавать объекты, выполняющие действия «от имени и по поручению» других объектов. Второй недостаток обходится с помощью ручного явного маршалинга указателей на интерфейс (так как мы это уже делали в этой статье) или с помощью GIT. Зато FTM-объекты дают значительный прирост производительности, ведь их методы вызываются напрямую (без proxy), и при вызове не происходит переключения потоков.

Neutral-модель – это более сложный механизм, позволяющий получить отдельный «COM+»-контекст, и при этом, так же, как и в случае с FTM-объектами, не производить переключения потоков. Более подробно Neutral-модель будет разобрана далее.

Как же задается потоковая модель? Потоковая модель определяется для компонента (CoClass'а) в реестре Windows. В HKEY_CLASSES_ROOT\CLSID\CLSID конкретного компонента \InprocServer32 может иметься значение с именем ThreadingModel. Если оно отсутствует, то компонент может создаваться только в Main STA (см. выше). Если значение присутствует, то оно может содержать следующие значения:

Значение

Где создается компонент

отсутствует

в Main STA

Apartment

в STA (Single-Threaded Apartment)

Free

в MTA (Multi-Threaded Apartment)

Neutral

в NTA (Neutral-Threaded Apartment. Поддерживается начиная с W2k)

Both

в апартаменте активатора (т.е. в любом)

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

Интересный эффект можно наблюдать, если изменить потоковую модель нашего компонента Server на Neutral (Это эксперимент можно проделать только под W2k).

При этом «OLE/COM Object Viewer» начинает показывать забавную картину (см. рисунок 9).


Рис. 9.

Дело в том, что «OLE/COM Object Viewer» не явно выводит в списке не описанный в библиотеке типов список поддерживаемых интерфейсов, а пробует получить это список динамически (видимо, просматривая список интерфейсов, зарегистрированных в реестре, и пытаясь их запросить у объекта).

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

Если теперь поставить точку прерывания в методе SimpleMethod, то мы получим call-стеки, показанные ниже:

  1. При вызове из того же потока:


    Обратите внимание на то, что появилось большое количество промежуточных вызовов в модулях rpcrt4 и ole32.

  2. При вызове из другого потока:


А во втором случае интересно не обилие промежуточных вызовов (их и раньше было немало), а то, что, в отличие от времени, когда компонент был зарегистрирован как поддерживающий поточную модель Apartment, контекст потока не переключается. Выполнение происходит в том же потоке!

При вызове методов из другого апартамента блокируется не только выполняемый в этот момент код, но и пользовательский интерфейс. Но эта блокировка не полная и ее можно управлять. Это происходит с помощью интерфейса IMessageFilter.

IMessageFilter – блокировка GUI при COM-вызовах

Для управления блокировкой интерфейса Windows предоставляет интерфейс IMessageFilter . Он позволяет COM-серверам и приложениям, взаимодействующим с этими серверами, выборочно обрабатывать Windows-сообщения во время ожидания ответов на синхронные COM-вызовы.

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

Асинхронные вызовы позволяют вызывающей стороне продолжать работать без ожидания ответа от вызываемого объекта. Пока объект обрабатывает асинхронный вызов, любой синхронный обратный вызов запрещён.

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

Вызовы объектов из главного потока приложения практически всегда синхронизированы по вводу. При обращении к удаленному объекту COM заблокирует большинство сообщений. По умолчанию будут обрабатываться такие сообщения, как WM_PAINT. Окна при этом не будут откликаться на ввод пользователя, и ему может показаться, что приложение зависло. Если пользователь будет настойчив, то реализация по умолчанию интерфейса IMessageFilter (встроенная в Windows) выдаст примерно такое диалоговое окно, как показано на рисунке 10.

Надо признаться – «кривоватая вещица». Этот диалог был создан для OLE2-приложений. Для распределенных COM-приложений он подходит не очень. Особо глупо выглядит кнопка «Switch To...». Тяжело переключится на приложение, работающее на другом компьютере, да ещё к тому же не имеющее GUI. Да ещё и красивая кнопочка «Cancel», как будто издеваясь, всем своим видом говорит – «Что, нажал?!».


Рис. 10.

Неудобства обработки по умолчанию не заканчиваются на этом диалоге. Иногда бывает неудобно, что нельзя изменить таймаут, после которого открывается этот диалог.

Все эти проблемы можно обойти, если реализовать свою обработку сообщений. Сделать это можно, создав свой COM-объект, реализующий интерфейс IMessageFilter и заменив (с помощью API-функции CoRegisterMessageFilter ) стандартную обработку. Создавать такой объект лучше на C/C++, ввиду его низкоуровневости, но можно попытаться сделать это и на Delphi.

Когда создать свой обработчик?

Реализация по умолчанию, предоставляемая СОМ, предлагает лишь минимальные возможности фильтрации сообщений. Впрочем, фильтрация сообщений теперь уже не так важна, как во времена 16-битных приложений (размер очереди сообщений теперь практически не ограничен).

COM будет вызывать вашу реализацию IMessageFilter, чтобы выяснить, заблокировано ли это приложение, и чтобы дать вам возможность отменить вызов или продолжить ввод в безопасные, с точки зрения удаленного вызова, поля вашего приложения. Например, вы можете вообще отказаться от выдачи диалогового окна. Вместо этого можно реализовать кнопку «отменить» и поместить ее в строку состояния вашего приложения.

Правда, есть одна проблема. При отмене сообщения важно, рассчитан ли на это удаленный объект. Если объект создан в STA, то после того, как вы в первый раз прервете выполнение некоторого метода, может статься так, что компонент не будет доступен для других вызовов. Дело в том, что «прерывание» метода не значит реального его останова. COM просто прекращает модальный цикл и передает управление вызывающей стороне, а метод удаленного компонента продолжает упорно трудиться, хотя его труд уже ни кому больше не нужен. Полностью эту проблему можно разрешить только под W2k (подробней смотри в номере нашего журнала за 2 квартал нашего года). В этой ОС имеется более продуманный подход к управлению требованиями отмены, исходящими из любого потока вызова. При маршалинге интерфейса создается proxy с встроенным cancel-объектом, реализующим интерфейс ICancelMethodCalls . Этот объект ассоциирован с вызовом и с потоком, где задерживается вызов. Но и под W2k для того, чтобы метод остановился, он сам должен проверить состояние вызова, что требует дополнительного кода. К тому же может сложиться ситуация, когда сам удалённый метод заблокирован. Например, он ожидает окончания вызова к другому COM-объекту, или он сделал не очень разумный запрос к БД и теперь безуспешно пытается дождаться ответа. Как бы там ни было, это тупик. Так что если вы предполагаете, что вызов метода может «зависнуть», то лучше сделать такой компонент многопоточным. При этом повисший метод может себе «висеть», а вы сможете спокойно выполнить ещё один вызов, ведь все вызовы к компоненту происходят в разных потоках и могут происходить параллельно. Если вы выбрали такую стратегию, то не забудьте убедиться, что для этого компонента отключена синхронизация. Да и вообще надо быть аккуратней, ведь данные такого компонента придется защищать вручную. Желательно дать возможность коду, производящему удаленный вызов, оповещать реализацию вашего фильтра, можно ли отменять вызов данного метода.

Но вернемся к реализации нашего обработчика... Хотя это, скорее всего, очевидно из описаний методов, стоит, видимо, подчеркнуть, что HandleIncomingCall – метод, предназначенный для разгребания сообщений внутри процесса объекта, а RetryRejectedCall и MessagePending – методы, предназначенные для работы на вызывающей стороне. Ясно, что объект должен располагать каким-то способом обработки вызовов, приходящих от удаленных клиентов. HandleIncomingCall предоставляет такую функциональность, позволяя объекту обрабатывать или откладывать обработку некоторых входящих вызовов, и отказываться от остальных. Клиент также должен знать, как объект собирается обойтись с его вызовом, чтобы реагировать соответственно ситуации. Клиенту, чтобы повторить вызов после некоторой паузы, нужно знать, отвергнут вызов объектом, или его обработка просто временно отложена. Клиент должен также уметь отвечать на сообщения Windows, одновременно ожидая ответов на задержку с ответом от сервера.

И так как я уже говорил, для регистрации фильтра сообщений надо использовать API-функцию CoRegisterMessageFilter . Вам нужно создать экземпляр вашего объекта, запросить у него указатель на IMessageFilter и передать его функции CoRegisterMessageFilter. После регистрации COM будет вызывать ваш фильтр сообщений вместо реализации, используемой по умолчанию. Напрямую этот интерфейс вызывать нет необходимости, это будет делать Windows.

Завершение работы приложений при WM_QUERYENDSESSION и WM_ENDSESSION

При завершении работы Windows каждое открытое приложение получает сообщение WM_QUERYENDSESSION, сопровождаемое сообщением WM_ENDSESSION, в случае, если закрытие сессии не отменено. Эти сообщения посылаются с помощью SendMessage , к сожалению, при этом запрещаются инициализацию все исходящие LRPC-вызовов. При создании серверного приложения следует отдельно протестировать поведение приложения при закрытии сессии и выключении компьютера.

Методы IMessageFilter Описание
HandleIncomingCall Предоставляет единую точку входа для входящих вызовов. Используется для отмены или откладывания обработки вызовов на серверной стороне.
RetryRejectedCall Дает приложению возможность вывода диалогового окна с предложением повтора, отмены или переключения задач (последний случай используется исключительно при взаимодействии с локальными приложениями типа Excel).
MessagePending Сообщает о появлении Windows-сообщения, в момент, когда COM ожидает ответа от сервера.
Таблица 1. Методы в порядке Vtable

Серверные приложения должны возвращать WM_QUERY­ENDSESSION TRUE без оповещения пользователя. При получении сообщения WM_ENDSESSION все COM-приложения должны выполнять нормальную процедуру закрытия каждого объекта приложения. В то же время, следует игнорировать любые ошибки, возникающие в результате межпроцессных вызовов или вызовов IUnknown::Release. Все указатели хранилищ (указатели интерфейсов IStorage и IStream) должны быть освобождены для корректного удаления любых временных файлов.

Метод HandleIncomingCall нам мало интересен потому, что, во-первых, он относится только к серверу, а во-вторых, он не позволяет отменить уже работающий вызов. Метод RetryRejectedCall вообще работает, если вы работаете с локальными OLE-приложениями, а вот MessagePending может быть нам очень интересен. Исходя из этого я дам только короткое описание методов HandleIncomingCall и RetryRejectedCall, а подробнее остановлюсь на методе MessagePending.

DWORD HandleInComingCall(
  //Тип входящего вызова
  DWORD dwCallType, 
  //Дескриптор задачи, вызывающей данную задачу
  HTASK threadIDCaller,
  //Прошедшее количество тиков
  DWORD dwTickCount,
  //Указатель на структуру INTERFACEINFO содержащую 
  //описание вызываемого интерфейса, объекта и метода
  LPINTERFACEINFO lpInterfaceInfo 
);

Возвращаемое значение

DWORD RetryRejectedCall(
   HTASK threadIDCallee,     //Дескриптор серверной задачи
   DWORD dwTickCount,     //Прошедшее количество тиков
   DWORD dwRejectType    //Возвращенное сообщение об отказе
);
Если клиент реализует IMessageFilter и вызывает серверные методы на удаленной машине, RetryRejectedCall не вызывается.

IMessageFilter::MessagePending

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

DWORD MessagePending(
//Дескриптор задачи вызываемого приложения
  HTASK threadIDCallee,
  DWORD dwTickCount,     //Прошедшее количество тиков 
  DWORD dwPendingType    //Тип вызова Call type
);

Параметры

Возможные возвращаемые значения

Примечания

COM вызывает IMessageFilter::MessagePending после того, как приложение сделало вызов COM-метода удаленного объекта, и получило сообщение Windows до возврата управления. Windows-сообщение может быть послано, например, когда пользователь выбирает команду меню или проводит мышью над некоторым окном. До того, как COM сделает вызов IMessageFilter::MessagePending , он подсчитывает время, прошедшее с момента совершения вызова метода удаленного объекта. Это время передается в параметре dwTickCount . В момент вызова метода MessagePending сообщение лежит в очереди сообщений приложения.

Сообщения Windows, появляющиеся в очереди вызывающей стороны, должны оставаться в очереди, пока не пройдет достаточно времени, чтобы убедиться, что это не результат опережающего ввода, а наоборот, попытка привлечь внимание. Используйте параметр dwTickCount для отслеживания задержки. Рекомендуется не менее чем двух- или трехсекундная задержка. В системах до W2k эта задержка была неприлично долгой и если вы, например, ввели неправильное имя удаленного компьютера, то можно было прождать несколько минут. Для обычного пользователя это неприемлемо долго! Так что даже только для изменения этой задержки имеет смысл сделать свой обработчик. Если время истекло, а вызов ещё не окончен, вызывающей стороне следует удалить сообщения из очереди, и показать диалоговое окно, предлагающее пользователю повторить вызов (ждать дальше) или отменить вызов (уведомив его об опасности этой операции).

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

Кое-какая обработка IMessageFilter::MessagePending сделана в MFC. Она не совершенна, но может послужить затравкой для вашей реализации. Вот она:

STDMETHODIMP_(DWORD) COleMessageFilter::XMessageFilter::MessagePending(
   HTASK htaskCallee, DWORD dwTickCount, DWORD /*dwPendingType*/)
{
   METHOD_PROLOGUE_EX(COleMessageFilter, MessageFilter)
   ASSERT_VALID(pThis);

   MSG msg;
   if (dwTickCount > pThis->m_nTimeout && !pThis->m_bUnblocking &&
      pThis->IsSignificantMessage(&msg))
   {
      if (pThis->m_bEnableNotResponding)
      {
         pThis->m_bUnblocking = TRUE;    // avoid reentrant calls

         // eat all mouse messages in our queue
         while (PeekMessage(&msg, NULL, WM_MOUSEFIRST, WM_MOUSELAST,
            PM_REMOVE|PM_NOYIELD))
            ;
         // eat all keyboard messages in our queue
         while (PeekMessage(&msg, NULL, WM_KEYFIRST, WM_KEYLAST,
            PM_REMOVE|PM_NOYIELD))
            ;

         // show not responding dialog
         pThis->OnNotRespondingDialog(htaskCallee);
         pThis->m_bUnblocking = FALSE;

         return PENDINGMSG_WAITNOPROCESS;
      }
   }

   // don't process re-entrant messages
   if (pThis->m_bUnblocking)
      return PENDINGMSG_WAITDEFPROCESS;

   // allow application to process pending message
   if (::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE|PM_NOYIELD))
      pThis->OnMessagePending(&msg);

   // by default we return pending MSG wait
   return PENDINGMSG_WAITNOPROCESS;
}

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

CoRegisterMessageFilter

Регистрирует в OLE объект, реализующий интерфейс IMessageFilter. Такая регистрация делается одна на весь процесс (EXE-модуль). Вопреки заверениям документации Microsoft, регистрировать обработчик можно и из DLL-модуля, но замечание о регистрации на весь процесс остается в силе.

Альтернативный подход

Чтобы обойти блокировку GUI, можно просто выполнять вызовы из другого потока, не связанного с пользовательским интерфейсом. При этом надо не забывать о маршалинге интерфейсов. Конечно, каждый вызов бессмысленно делать из отдельного потока. Имеет смысл делать это с вызовами, при которых велика вероятность больших задержек или подвисаний. При работе с БД имеет смысл задавать таймаут для выполнения SQL-запросов. Бросая выполняющийся вызов, надо всегда помнить о том, что «подвисший» вызов может занимать большое количество ресурсов (памяти, процессорного времени, ресурсов и т.п.).

Итак, мы разобрались, что такое апартамент, с чем его едят, и как им управлять. Вы заметили, что по ходу дела то тут, то там встречались замечания насчет изменений, вносимых в СОМ в W2k. В следующем разделе мы подробным образом рассмотрим изменения, вносимые W2k в потоковые модели COM и наконец-то узнаем – «что такое контекст ? ».

W2k и COM

С каждой новой версией Windows NT, начиная с версии 3.5, в модель COM вносились изменения. В NT 3.5 появилась первая 32-битная реализация COM, хотя реально работать с ней мог только один поток в процессе. NT 3.51 (и Windows 95) позволили использовать COM из любого потока в процессе, а также представили концепцию апартамента (apart­ment), разобранную нами выше.

Версия COM в NT 4.0 возвестила о новом многопоточном типе апартамента, лучшей интеграции с системой безопасности NT, и новом IDL-компиляторе, избавляющем от раздельных IDL и ODL-файлов. В NT 4.0 также появилась поддержка кросс-машинного взаимодействия. Но Distributed COM оказал относительно небольшое воздействие на модель программирования, которая поддерживала межпроцессные вызовы с момента возникновения СОМ.

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

Для начала вернемся к многопоточности и попробуем разобраться с вопросами, возникающими при работе старыми способами и с их решением в W2k.

Например, разработчик компонента хочет, чтобы вызовы методов его компонента производились последовательно (то есть необходимо запретить параллельный вызов). Под NT 4.0 или более ранней версией есть несколько способов добиться этого. Программист может использовать критические секции Win32 API в коде методов своего компонента. Другой путь состоит в простой описание класса как Thre ­a­ding­Model=Ap­artment, после чего COM гарантирует отсутствие параллельного доступа. Первый подход использует для достижения цели явное программирование для данной платформы. Второй подход не требует явного кода, поскольку платформа (в данном случае COM) неявно обеспечивает нужный сервис.

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

Но все это не вполне объясняет, чем ThreadingModel=Apar­tment лучше использования критических секций, так как оба возлагают на платформу выполнение значительной части работы. Причина тут в некоторой степени тонкая. Использование критических секций требует размазать по всей реализации компонента маленькие кусочки исполняемого кода. Каждое дробление — неважно, какого размера — выливается в очередную возможность сделать простую человеческую ошибку. ThreadingModel=Apartment требует ровно нуль строк кода. Это значит, что шансов написать глючную строку, исправляемую только перекомпиляцией, у вас нет. Это вдобавок означает, что если нижележащая платформа поддерживает необходимую функциональность, ваш код будет работать.

Надо заметить, что задание ThreadingModel=Apartment дает программисту большую независимость от производителя платформы – в данном случае Microsoft. Из-за отсутствия кода, полагающегося на жестко кодированные функция/метод сигнатуры или семантику, разработчик платформы более свободен изменять нижележащую реализацию, не разрушая вашего кода. Теоретически, пока семантика сервиса не изменяется, ваш код будет продолжать работать. Главное достоинство такого декларативного подхода в том, что он представляет наименьшую из возможных зависимость между компонентом и платформой.

Но у ThreadingModel=Apartment есть и свои недостатки. Во-первых, производительность. Переключение с потока на поток не проходит бесследно, особенно при внутрипроцессных вызовах. Сравните это со случаем, когда для синхронизации используют примитивы Windows, такие, как мьютексы или семафоры. В этом случае обработка вызова происходит в том же потоке, что и вызов, и если, соответственно, используемый примитив не заблокирован (другим вызовом), то никаких непроизводительных действий не происходит.

ThreadingModel=Apartment – канонический пример декларативного описания ваших намерений вместо жестко определенного кода. Это та самая идея, что проходит сквозь всю программную модель MTS. Вместо написания кучи кода, реализующего обработку транзакций, сериализацию или безопасность, MTS использует декларативные атрибуты для управления поведением компонента и расширения его возможностей. Это не значит, что вам вовсе не придется писать кода; это, скорее, означает, что количество кода, необходимого для использования этих сервисов, существенно уменьшается. MTS поднимает уровень абстракции, используемый в разработке компонентов, в первую очередь за счет концепции атрибутов. W2k вносит фундаментальный сдвиг в COM – формализацию декларативного программирования с использованием классов, функциональность которых определяется с помощью задания атрибутов, и контекста как части модели программирования COM.

Менеджер каталогов

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

MTS хранил информацию о конфигурации компонента отдельно от COM HKEY_CLASSES_ROOT\CLSID. Средства управления конфигурацией (включая MTS Explorer) использовали Catalog Manager для управления конфигурацией класса. Как показано на Рис 11, MTS Catalog Manager был scriptable-компонентом, который не только хранил атрибуты класса, но также и имя файла DLL, содержащей код класса. Это потребовалось потому, что Catalog Manager должен был переписать InprocServer32-вхождение вашего компонента, чтобы оно указывало не на вашу DLL, а на MTS Executive. CoCreateInstance будет читать эту информацию и вместо вашего компонента загружать MTS-подсистему, которая и будет взаимодействовать с вашим компонентов. На MTS Executive лежит выполнение всех сервисов, настроенных вами.


Рис. 11. Менеджеры каталогов.

Под W2k все должно работать более-менее так же, как под MTS. Как показано на рисунке 11, COM предоставляет Catalog Manager, управляющий не только HKEY_ CLA­SSES_ROOT\CLSID, но и вспомогательной БД конфигураций (сейчас именуемой RegDB). Заметьте , что под W2k имя DLL-файла компонента может оставаться в InprocServer32. Поддержка конфигураций компонентов встроена в COM; MTS Executive больше не нужен. В момент активации CoCreateInstance просматривает каталог, чтобы понять, какие дополнительные сервисы нужны вашему классу (если вообще нужны).

Не все COM-компоненты регистрируются в Catalog Manager. Когда компонент саморегистрируется, например, с помощью REGSVR32.EXE, используется Registry API для вставки ключа InprocServer32, и, обычно, вхождения ThreadingModel, компонент рассматривается как неконфигурируемый или legacy-компонент. Расширенные атрибуты (такие, как синхронизация и транзакционность) могут иметь только явно инсталлированные в Catalog компоненты. Такие компоненты называются конфигурированными.

Атрибут Значения Уровень
Must activate in activator’s context. Вкл./Выкл. Класс
Transaction Nonsupported, Supported, Required, Requires New Класс
Synchronization Nonsupported, Supported, Required, Requires New Класс
Object Pooling On/Off, Max Instances, Min Instances, Timeout Класс
Declarative
Construction
Arbitrary Class-specific
String
Класс
JIT Activation Вкл./Выкл. Класс
Activation-time
Load Balancing
Вкл./Выкл. Класс
Instrumentation Вкл./Выкл. Класс
Декларативная настройка защиты
(Declarative Authorization)
Ноль или более имен ролей Класс, Интерфейс, Метод
Auto-Deactivate Вкл./Выкл. Метод
Таблица 2. Опции конфигурации.
Attribute Setting
Activation Type Library (внутрипроцессный)/Server (суррогатный)
Authentication Level None, Connect, Call, Packet, Integrity, Privacy
Impersonation Level Identify, Impersonate, Delegate
Authorization Checks Application Only/Application + Component
Security Identity Interactive User/Hardcoded User ID + PW
Process Shutdown Never/n minutes after idle
Debugger Command Line to Launch Debugger/Process
Enable Compensating Resource Managers Вкл./Выкл.
Enable 3GB Support Вкл./Выкл.
Queueing Queued/Queued+Listener/RemoteServerName
Таблица 3. Атрибуты: Приложения

Таблицы 2 и 3 показывают опции конфигурации в W2k. Таблица 2 показывает атрибуты, которые могут быть заданы для класса, интерфейса или метода. COM поддерживает идею приложения-коллекции COM-классов, разделяющих определенные установки конфигурации. В таблице 3 приведены такие разделяемые атрибуты и их возможные значения.

Есть два способа сконфигурировать компонент. Можно использовать Component Services Explorer. Можно также программировать вручную, используя интерфейсы, предоставляемые Catalog Manager. В дополнение, за исключением транзакционности (что может быть задано в IDL-файле), разработчик компонента не может управлять исходными атрибутами, используемыми при инсталляции компонента в Catalog Manager.

Основы перехвата

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

  1. Компонент описывает, что ему нужно для нормального функционирования, используя декларативные атрибуты.
  2. При вызове CoCreateInstance система проверяет, работает ли активатор в окружении, совместимом с конфигурацией класса.
  3. Если среда активатора пригодна, никакого перехвата не нужно и CoCreateInstance просто возвращает указатель на реальный интерфейс.
  4. Если среда активатора несовместима с компонентом, CoCreateInstance переключает управление на среду, совместимую с целевым классом, создает там объект и возвращает указатель на proxy.
  5. Proxy производит некие магические действия до и после каждого вызова метода, чтобы убедиться, что среда исполнения совместима с требованиями класса при исполнении метода.

Вот так. Перечитайте эти пять пунктов еще раз. Идея перехвата – один из краеугольных камней современного COM-программирования и главная мысль остальной части этой статьи.

Чтобы конкретизировать давайте посмотрим, как эти пять шагов применяются в NT 4.0. Представьте класс, хранящийся в DLL и зарегистрированный как ThreadingModel=Apartment.

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

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

Перехват и контексты

Как показано на рис. 14 , COM под W2k разбивает процесс на контексты. Контекст – это коллекция объектов, разделяющих требования к среде исполнения. Поскольку разные классы могут быть сконфигурированы с различными требованиями, процесс часто содержит более одного контекста, чтобы разделить несовместимые объекты. Некоторые конфигурационные установки позволяют объекту находиться в одном контексте с его единомышленниками. Другие конфигурационные установки заставляют объект жить в собственном контексте, где никогда никого больше не появится. Единственное исключение из всего этого – вызов CoCreateInstance для несконфигурированного класса всегда приводит к созданию объекта в контексте активатора (при условии, что атрибут класса ThreadingModel совместим с типом апартамента активатора).


Рис. 12. Контексты процесса в COM.

Каждый контекст в процессе содержит уникальный представляющий его COM-объект. Этот объект называется контекстным (object context, OC). Объекты могут обращаться к контекстным объектам своих контекстов через API CoGetObjectContext. Через контекстные объекты компоненты взаимодействуют с сервисами, предоставляемыми их контекстами, например, транзакционностью и JIT-активацией, как правило, используя интерфейс IObjectContextInfo.


Рис. 13. Использование Proxу.

Как показано на рис. 13, чтобы позволить объектам производить вызовы через границы контекстов, используются proxy. Эти proxy производят все действия по перехвату, необходимые для переключения среды исполнения с конфигурации вызывающей стороны на конфигурацию вызываемого объекта. Такие сервисы перехвата могут включать управление тразакциями и блокировками, манипуляцию потоков, JIT-активацию или что-нибудь еще более экзотическое.

Proxy нужны всякий раз, когда объект вызывают из-за границ контекста. Под W2k практически все объектные ссылки, возвращенные вызовами функций или методов API контекстно-зависимы. Это означает, что ссылка, полученная от CoCreateInstance (или любого вызова API или метода COM) может использоваться только в текущем контексте. Объясняется это довольно просто.

Рассмотрим случай, когда CoCreateInstance возвращает простую ссылку. Это значит, что объект находится в текущем контексте и зависит от текущей среды исполнения. Недопустимо использовать эту ссылку в другом контексте без явного маршалинга ее в другой контекст. Например, простое хранение ссылки в глобальной переменной для использования объектами в других контекстах совершенно недопустимо. Если объекту в другом контексте придется использовать такую ссылку, методы объекта будут исполняться, не используя преимуществ перехвата. Это значит, что вместо среды, на которую рассчитывает вызываемый объект, будет использоваться среда исполнения вызывающей стороны. Если объект полагается на транзакцию, как часть своей среды исполнения, не тут то было! Транзакции может не быть, а то и хуже, объект может заупрямиться и работать в транзакции вызывающей стороны (которая может быть другой транзакцией). Практически все другие сконфигурированные сервисы также будут работать неправильно, если вызов обрабатывается не в том контексте.

Относительность контекстов

Чтобы понять, почему объектные ссылки по-прежнему контекстно-зависимы, даже если относятся к proxy, требуются дополнительные объяснения. Proxy, возвращаемые любой API-функцией или вызовом COM-метода, настроены на исполнение определенного набора сервисов перехвата, основанных на различиях между контекстом объекта и контекстом, где инициализируется ссылка. Передача этой proxy в другой контекст не гарантирует работы, так как этот третий контекст может требовать совершенно других сервисов перехвата для правильного переключения между контекстами исполнения. Вы, конечно, можете возразить, что proxy должна быть достаточно сообразительной для работы в любом контексте, но это усложняет реализацию proxy и снижает ее эффективность. Кроме этого, ввод такой поддержки в proxy сделает модель программирования более сложной, так как ссылки на proxy придется рассматривать не так, как прямые ссылки на реальные объекты. Подведем итог, не используйте объектные ссылки в разных контекстах без маршалинга.


Рис. 14. Передача объектной ссылки между контекстами

Для передачи объектных ссылок из контекста в контекст в COM предусмотрено две API-функции, CoMarshalInterface и CoUnmarshalInterface, которые переводят контекстно-зависимые объектные ссылки в контекстно-нейтральные потоки байтов и обратно. Эти потоки байтов можно свободно передавать в любой контекст. В общем, прикладные программисты практически никогда не делают этих вызовов явно. Эти процедуры автоматически вызывает CoCreateInstance при создании объекта для преобразования прямого указателя на интерфейс в указатель на proxy, пригодную для использования в контексте активатора (см. рисунок 14). Для упрощения стыковки компонентов, каждый раз, когда proxy видит объектную ссылку как параметр метода, proxy производит маршалинг объектной ссылки, чтобы обеспечить корректность использования ссылок (см. рисунок 15). И только при использовании объектной ссылки вне области действия метода следует позаботиться о явном маршалинге и демаршалинге объектных ссылок между контекстами, используя CoUnMarshalInterface или более экзотические способы типа Global Interface Table (GIT, средства из библиотеки COM, переводящего контекстно-зависимые объектные ссылки в нейтральные DWORD и наоборот).

Цена использования внутрипроцессной proxу под NT 4.0 довольно высока с точки зрения производительности. Цена таких proxy в NT 4.0 (я буду называть их здесь тяжеловесными proxy) обуславливается переключением потоков ОС, необходимым для пересечения границ апартаментов. Сериализация стека вызовов также влияет на производительность вызовов под NT 4.0, но главная составляющая цены все-таки переключение потоков. Кросс-контекстные prox y, которые используются под W2k, не обязательно требуют переключения потоков или сериализации стека вызовов. Все, что требуется proxy для пересечения границ контекста – работа тех сервисов перехвата, которые требуются для пересечения именно этой границы (помните те 55 вызовов из первой части статьи, где мы зарегистрировали наш компонент как NTA). Если объектные ссылки передаются как параметры методов, proxy должна производить их маршалинг через границы контекста; в противном случае стековый фрейм может просто быть разделен между границами контекстов.


Рис. 15. Маршалинг объектных ссылок

Несмотря на то, что межконтекстные вызовы относительно дешевы по сравнению с межапартаментными вызовами под NT 4.0, можно сконфигурировать класс для активации в контексте его создателя. Это полезно для библиотечных компонентов, которые хотят работать в контексте их создателя и не нуждаются в настройке собственных сервисов. Если по каким-то причинам контекст создателя был сконфигурирован так, что не может поддерживать новых объектов, Co­Cre­ate­Instance потерпит неудачу и возвратит CO_E_AT­TEM­PT_TO_CRE­ATE_OUTSIDE_CLIENT_CONTEXT.

Если вызов CoCreateInstance завершился успешно, все вызовы нового объекта будут обслуживаться в контексте создающего компонента (даже если ссылки на новый объект передаются другим контекстам). Это, кстати, поразительно похоже на создание экземпляра несконфигурированного класса. Главная разница в том, что для несконфигурированных классов COM может разрешить использование второго контекста из-за таких проблем, как несовместимые поточные модели, что выливается в возврат proxy вместо ошибки CO_ E_AT­TEMPT_TO_CREATE_OUTSIDE_CLIENT_CONTEXT.


Рис. 16. Нейтральность к контексту

Если объект хочет всегда исполняться в контексте создателя, даже если ссылки на него передаются в другие контексты, он должен агрегировать свободно-поточный маршалер (freethreaded marshaler, FTM), возвращаемый функцией Co ­Cre­ateFreeThreadedMarshaler. Такой объект обманывает W2k и становится контекстно-нейтральным. При маршалинге такого интерфейса в контекстно-нейтрольный поток байтов записывается физический адрес указателя на интерфейс (рисунок 16). Заметьте, что никакой контекст никогда не получит proxy контекстно-нейтрального объекта, даже при маршалинге ссылок на объект через границы контекстов. Одна из причин отказа от proxy или перехвата – производительность. Однако, более вероятная причина в том, что компонент должен выполнять некие обслуживающие функции, что требует прямого доступа к среде вызывающей стороны (как, например, в случае компонента, одновременно используемого транзакциями разных контекстов), даже при использовании в разных контекстах.

Контексты и апартаменты

Тут ветераны COM-программирования, возможно, спросят – не заменяют ли контексты апартаменты. Ответ – и да, и нет. Та часть, которая «да», говорит, что контексты заменяют апартаменты как самую дальнюю внутреннюю область действия объекта. Объектные ссылки теперь контекстно-зависимы — не только апартаментно-зависимы — а функцкция CoMarshalInterface и GIT теперь используются для кросс-контекстной работы, а не только для межапартаментного взаимодействия. Все это не означает исчезновения апартаментов. Роль апартаментов в модели программирования уменьшится, но они продолжат своё существование.


Рис. 17. Апартаменты, контексты и процессы

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

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

Алгоритм определения, в каком апартаменте создан объект, не изменился со времен NT 4.0. Чтобы определить, в каком апартаменте создавать объект, CoCreateInstance проверяет атрибут ThreadingModel целевого класса. Если поточная модель совместима с текущим апартаментом, новый объект создается в этом апартаменте. В противном случае COM создает объект в апартаменте подходящего типа. Для активирующих вызовов внутри одного апартамента COM попытается создать новый объект в контексте создателя, если только конфигурация целевого класса не запрещает этого.

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

Напомню, что CoInitializeEx принимает параметр, говорящий, к какому типу апартамента будет относиться поток. Передача COINIT_MULTITHREADED говорит COM поместить поток в единственный многопоточный апартамент. Передача неудачно названного флага COINIT_APARTMENTTHREADED велит COM поместить поток в новый STA, куда никакому другому потоку сроду не попасть.

STA предназначены для кода интерфейса пользователя и в обработке входящих вызовов полагаются на очередь сообщений Windows. Чтобы обеспечивать отсутствие блокировок при вызовах, производимых из STA, COM запускает подкачку сообщений Windows в процессе ожидания возврата вызова, позволяя обрабатывать входящие вызовы и основные оконные сообщения (такие, как WM_NCACTIVATE). Кроме того, потоки в STA не могут выполнять блокирующих операций (WaitForSingleObject, например) без периодической обработки сообщений во избежание блокировки.

И MTA, и STA связывают набор потоков в набор контекстов. В случае MTA набор потоков – это все потоки, вызывавшие CoInitializeEx(COINIT_MULTITHREADED). Для STA набор потоков состоит из одинокого потока, вызвавшего CoInitializeEx(COINIT_APARTMENTTHREADED) для создания апартамента. Поскольку потоки процесса разбиты на апартаменты, NT 4.0 не предоставляла простого способа указать, что к компоненту можно свободно обращаться из любого потока в процессе, независимо от апартамента, к которому принадлежит поток. В W2k все изменилось.


Рис. 18. Thread-neutral апартаменты.

Microsoft в W2k ввел третий тип апартамента, нейтральный к потокам (thread-neutral apartment, TNA). Как показано на рисунке 18, каждый процесс содержит не более одного TNA. Классы указывают, что они хотят работать в TNA, используя установку ThreadingModel=Neutral в реестре. В отличие от MTA и STA, ни один поток не может назвать TNA своим домом. Вместо этого, когда поток, выполняющийся в MTA или STA, создает объект с ThreadingModel=Neutral, он получает легковесную proxy, переключающую на контекст объекта без переключения потоков. Ни один поток процесса никогда не нуждается в выполнении переключения потоков при вхождении в TNA. В W2к ThreadingModel=Neutral должен стать предпочтительным для компонентов, не имеющих пользовательского интерфейса. (Компоненты интерфейса пользователя должны быть по-прежнему помечены как Threa­dingModel=Apartment).

Поначалу разработчики часто путают TNA со свободнопоточным маршалером (freethreaded marshaler). С высоты 10,000 метров они и впрямь выглядят похоже, поскольку оба обеспечивают обслуживание входящего вызова вызывающим потоком. Фундаментальное различие в том, что TNA-объекты по-прежнему зависят от апартаментов (и контекстов). То есть они принадлежат контексту и могут хранить контекстно-зависимые ресурсы, например, объектные ссылки. Напротив, FTM-объекты контекстно-нейтральны и не имеют собственного контекста (они используют контекст вызывающей стороны). FTM-объекты не могут содержать контекстно-зависимых ресурсов. В общем, FTM предназначен для весьма специфических применений, а TNA предпочтителен в большинстве случаев.

CoCreateInstance
вызван из:
Класс регистрирован как ThreadingModel
Apartment Both Free отсутствует Neutral
Новый объект активирован в
STA-поток в текущем STA Текущий апартамент MTA Main STA TNA
TNA
(STA-поток)
MTA в отдельном STA
TNA
(MTA-поток)
Таблица 4.

Вас может заинтересовать, как изменяется интерпретация установок в случае нового типа апартамента. Таблица 4 показывает, в каком апартаменте окажется новый объект во всех возможных ситуациях. Заметьте, что Thre­a­ding­Model=Both на самом деле означает "создайте меня в апартаменте моего активатора". Как и под NT 4.0, это существенно отличается от нейтральности к апартаментам (или контекстам). ThreadingModel=Both просто значит, что новый объект будет инициализирован в апартаменте активатора, и все последующие вызовы методов будут также обслуживаться там. Использование FTM для эмуляции апартаментной и контекстной нейтральности имеет совершенно другое значение. FTM говорит, что этот объект должен использовать контекст, из которого вызван его метод. Хотя ThreadingModel=Both и FTM часто используются совместно, они решают совершенно разные проблемы и могут использоваться порознь.

Контексты и активности

Задание ThreadingModel=Neutral не значит, что вопросы синхронизации теперь возлагаются на программиста. В W2k появился механизм синхронизации более простой и эффективный, чем апартаменты. Это также означает, что синхронизация может осуществляться не только на уровне апартамента, но и на уровне контекста.

Под W2k объекты говорят, что им нужен синхронизированный доступ, используя расширенный атрибут Synchro­ni­za­tion. Атрибут Synchronization в большой степени независим от значения ThreadingModel. Появление нового способа синхронизации не означает, что для синхронизации теперь нельзя использовать STA-модель, но новая концепция более эффективна и удобна. Установка ThreadingModel говорит, какие потоки в процессе могут осуществлять вызовы реального объекта. Атрибут Synchronization контролирует время отправки этих вызовов. Одновременной установкой Syn­chronization=Required и ThreadingModel=Neutral можно достичь модели, в которой любой поток может вызвать объект, но только по очереди. Понять, как атрибут Synchro­ni­za­tion влияет на модель программирования, можно, посмотрев на нижележащую абстракцию, контролируемую этим атрибутом. Эта абстракция называется активностью (activity). Она унаследована от MTS.

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


Рис.19. Примеры активностей

Как показано на рисунке 19, каждый контекст в процессе принадлежит максимум одной активности, а некоторые контексты не входят ни в какие активности вообще. Контексты, не входящие в активность (такие, как контекст по умолчанию) не получают внутренней сериализации вызовов; это значит, что любой поток из того же апартамента, что данный контекст, может войти в контекст в любой момент. Конечно, если апартамент контекста – STA, более одного потока в нем быть не может. Но если это MTA или TNA, объектам контекста лучше приготовиться к параллельному доступу. Активности сильно отличаются от апартаментов, поскольку активности могут содержать контексты из нескольких процессов и хостов.

Таблица 5 показывает, как атрибут Synchronization влияет на то, в какой активности будет жить новый объект (если будет). В общем, новый объект будет помещен в активность создателя, в новую активность или останется вне всякой активности. Классы, отмеченные как поддерживающие JIT-активацию или транзакции, требуют активности.

Установки синхронизации для нового класса Имеет активность? Входит в активность создателя?
NOT_SUPPORTED Никогда Никогда
SUPPORTED Если создатель входит в активность Если создатель входит в активность
REQUIRED Всегда Если создатель входит в активность
REQUIRES_NEW Всегда Никогда
Таблица 5.

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

Активности используют понятия причинности COM для предотвращения взаимоблокировок. Причинность COM – это простая цепь вложенных вызовов методов в иерархии объектов. Рассмотрим случай, где метод A вызывает метод B, который затем вызывает метод C, вызывающий метод D. Вызовы B, C и D являются вложенными по отношению к вызову A. В терминах COM мы скажем, что все четыре вызова принадлежат к одной причинности и связаны. COM автоматически отслеживает причинность, помечая все кросс-контекстные вызовы методов идентификаторами причинности, которые следует за цепью вызовов от объекта к объекту — даже с хоста на хост.

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

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

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

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

Педанты, читающие эту статью, наверняка уже заметили веселое словцо «практически». Я использую слово «практически» потому, что есть по крайней мере одна проблема с параллелизмом, не решаемая активностями. Представьте случай, когда поток X вызывает объект A, а поток Y вызывает объект B, где A и B находятся в одной активности. В идеале, вызов либо X либо Y получит доступ первым, блокируя запрос другого потока до полной обработки первого вызова (включая все вложенные вызовы). Из предыдущего объяснения следует, что если A и B находятся в одном процессе, всё произойдет именно так.

Если же объекты A и B находятся в двух разных процессах, вполне возможно, что запросы X и Y будут выполняться параллельно, поскольку кросс-процессная блокировка отсутствует. Хуже того, если объекты A и B вызовут друг друга, взаимоблокировка почти неизбежна, поскольку X и Y представляют две разных причинности и не рассматриваются как вложенные вызовы. Это одна из причин, по которым объекты условно не разделяются разными клиентами. Точнее, приложения MTS и COM+ обычно основаны на частных объектах, защищенных транзакциями от состояния совместного доступа.

Контексты, активности и потоки транзакций

Активности весьма эффективны для управления параллелизмом в пределах процесса, но им присущи некие суровые ограничения, когда дело доходит до управления параллелизмом за границей процесс/хост. Чтобы преодолеть эти ограничения, в COM имеется дополнительная абстракция для управления параллелизмом через границы процесса и хоста: транзакции и потоки транзакций.

Как и причинности, транзакции существуют во времени, и, подобно активностям – в пространстве. Транзакция – это временный набор операций защищенных свойствами ACID (атомарность, непротиворечивость, изоляция и долговечность – atomicity, consistency, isolation, and durability). В управлении транзакциями COM полагается на Distributed Transaction Coordinator (DTC). О концепции транзакций уже написано достаточно, включая несколько солидных томов (до русского читателя они пока не доехали – прим.ред.). Однако о потоках транзакций со времени их появления в MTS 1.0 написано куда меньше.


Рис. 20. Потоки транзакций.

Потоки транзакций – это коллекция из одного или нескольких контекстов в пространстве, разделяющих одну транзакцию. Как показано на рисунке 20, поток транзакций полностью содержится внутри активности, но активность может содержать более одного потока транзакций. Все объекты в потоке транзакции в определенный промежуток времени разделяют доступ к одной транзакции. Поскольку транзакции непостоянны, COM автоматически начинает новую транзакцию, если предыдущая транзакция потока закончилась. Объекты имеют доступ к своей транзакции, используя IObjectContextInfo::GetTransaction для поддержки ресурсов, нужных для ручного запуска транзакции. Более важно, что транзакционное ПО (например, ODBC, OLE DB или Microsoft Message Queue) могут получить доступ к транзакции контекста автоматически, когда объект пытается использовать транзакционные ресурсы (БД или очередь сообщений). Такой автозапуск – основа декларативного стиля программирования, пронизывающего COM под W2k.

В то время, как сам COM мало что делает с транзакциями, кроме того, что делает их доступными для транзакционных объектов и той «сантехники», с которой они работают, DTC занимается координацией результатов транзакций. Сверх очевидных характеристик атомарности, относящихся к откатам, DTC реализует стратегию управления блокировками, основанную на блокировках родственных транзакций (transaction-affinity locks). Это значит, что блокировка содержится в менеджере ресурсов транзакции (например, БД), и доступна для повторного входа из любого места потока транзакций, поскольку все объекты потока транзакций разделяют одну транзакцию DTC. В дополнение, менеджеры ресурсов транзакций обычно используют двухфазную стратегию блокировки, поддерживающую непротиворечивость состояния до окончания транзакции. Такой стиль управления блокировкой очень сложно реализовать без помощи менеджеров транзакций типа DTC.

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

Когда объект отключен от клиента, мы называем его деактивированным. В общем, объекты деактивируются когда клиент освобождает последнюю ссылку на объект. Однако любой объект, поддерживающий JIT-активацию, может ускорить свою деактивацию вызовом IContextSta­te::SetDe­ac­tivateOnReturn контекстного объекта. Этот метод задает или удаляет бит "done" контекста, который, если задан, говорит COM отключить текущий экземпляр объекта от клиента, как только закончится выполнение вызванных методов. При следующем клиентском вызове через неотключаемую proxy, COM тихо подключит другой (новый) экземпляр того же класса для обслуживания вызова.

Комбинация JIT-активации и транзакций очень интересна. Когда корневой объект потока транзакций вызывает SetDeactivateOnReturn(TRUE), это значит, что он хочет закончить текущую транзакцию. Стоит этому произойти, COM создаст новую транзакцию для потока при поступлении очередного вызова любому объекту в потоке транзакций. Запомните, что единственный способ завершить эту вторую транзакцию – деактивировать объект в корневом контексте. Это значит, что кто-то должен в процессе второй транзакции вызвать метод корневого объекта для запуска деактивации и фиксации транзакции. Обратите внимание, что корневой контекст создается в момент создания объекта, а не в момент вызова метода. Это значит, что клиенты должны помнить, какие объекты являются корневыми, чтобы обеспечить своевременное завершение последующих транзакций в потоке.

Любой объект в потоке (корневой или нет) может предотвратить завершение транзакции, очистив "happy" бит своего контекста. Каждый контекст в потоке транзакций следит за тем, довольны ли его объекты текущим состоянием транзакции. Объект может установить или удалить этот бит, используя IContextState::SetMyTransactionVote. Транзакция может быть зафиксирована, только если все контексты в потоке транзакций «счастливы». Когда корневой объект деактивируется, COM опрашивает все контексты в потоке транзакций. Если бит счастья одного или более контекстов сброшен, транзакция будет прервана с откатом всех изменений. Заметьте, что этот бит опрашивается только при деактивации корня, поэтому большинство методов оставляет этот бит неустановленным, полагаясь на то, что один из последующих вызовов методов установит этот бит, позволяя дальнейшие изменения в транзакции.

Интересное исключение из этого правила возникает, когда объект возвращает управление с пустым «счастливым» битом и установленным битом «done». Это говорит COM, что объект обнаружил ошибку, которую не может исправить, и считает текущую транзакцию проваленной. Заметьте, что оба эти метода IContextState просто управляют двумя битами в Thread Local Storage (TLS). Эти биты не опрашиваются, пока последний метод не возвращает управления COM.

Классы управляют своими отношениями с потоками транзакций , используя расширенные атрибуты. Как показано в Таблице 6, новый объект будет находиться в потоке транзакций своего создателя, новом потоке транзакций или вообще вне потоков транзакций. Примечательно и то, что если пометить класс как TRANSACTION_REQUIRES_NEW, это приведет к появлению объекта, который заведомо становится корнем потока транзакций (и поэтому должен вызывать SetDeactivateOnReturn, чтобы заставить транзакцию завершиться). Если пометить класс как TRANSACTI­ON_SUP­POR­TED, то объект никогда не станет корнем потока транзакций (а потому имеет мало причин вызывать SetDeactivateOnRe­turn, по крайней мере в отношении времени транзакции). Обозначение класса как TRANS­AC­TI­ON_REQUIRED дает объект, который может стать корнем потока транзакций, а может и не стать.

Установки транзакций для нового класса Принадлежит потоку транзакций? Разделяет поток транзакций создателя? Корень потока?
NOT_SUPPORTED Никогда Никогда Никогда
SUPPORTED Как и создатель Как и создатель Никогда
REQUIRED Всегда Как и создатель Как и создатель
REQUIRES_NEW Всегда Никогда Всегда
Таблица 6.

Наконец, заметьте, что для любых двух потоков транзакций результат любых их транзакций совершенно независим. Вопреки тому, во что вы интуитивно верите, вторая транзакция, вытекающая из TRANSACTION_REQUIRES_NEW – не вложенная транзакция. Наоборот, это абсолютно независимая транзакция, никак не связанная с транзакцией ее создателя (если, конечно, первая транзакция не заметит результата второй и не каскадирует провал второй транзакции явным вызовом SetMyTransactionVote).

MTS не позволяет транзакционным объектам узнать результаты их транзакций. Это должно быть справедливо и для W2k, где объекты дают пассивное согласие через «бит счастья», в блаженном неведении о том, приняты или нет результаты работы потока транзакций. Поскольку у них нет ни малейшего понятия об успехе или провале их транзакции, все объекты в потоке транзакций деактивируются в границах транзакции, чтобы исключить любое влияние на изоляцию транзакции. Это делается в целях повышение эффективности и упрощения модели программирования. Может случиться, что транзакционный объект захочет выполнить какой-то дополнительный код в конце транзакции, чтобы повлиять на ее исход, и/или чтобы выполнить компенсирующую транзакцию в случае провала транзакции. W2k включает поддержку пользовательских компенсаторов (Compensating Resource Managers или CRM) чтобы упростить это по сравнению с MTS.

Компенсатор – это нетранзакционный объект, используемый системой для представления транзакционного компонента в конце транзакции. Транзакционные объекты, которые хотят использовать компенсаторы, используют предоставляемый системой лог-менеджер с названием CRM Clerk. Clerk записывает указанные пользователем CLSID нужного компенсатора наряду с переменным количеством BLOB’ов, которые транзакционный объект пишет в лог в процессе нормальной работы. После завершения транзакции система создает экземпляр указанного пользователем класса-компенсатора.

Затем система использует интерфейс компенсатора ICrmCompensator для выполнения желаний транзакционного объекта в процессе двухфазной фиксации транзакции. Это значит, что компенсаторы (как любые менеджеры ресурсов) могут прервать транзакцию на подготовительной фазе. Это значит также, что компенсатор (как любой менеджер ресурсов) оповещают о возможном исходе транзакции во второй фазе. В случае успешного завершения компенсатор обычно освобождает все занятые им ресурсы. В случае отмены транзакции компенсатор может создать вторую компенсирующую транзакцию для отмены работы, проделанной транзакционным объектом (или компенсатором во время подготовительной фазы). Управление всеми тонкостями во время восстановления полностью ложится на систему.

Наконец, чтобы дать большую свободу разработчикам приложений, W2k поддерживает возможность, названную Bring Your Own Transaction (BYOT). BYOT позволяет ассоциировать контролируемые приложением транзакции с потоком транзакций. Интересное применение BYOT – ручное создание DTC-транзакции с произвольным размером таймаута и ассоциирование нового транзакционного объекта с долгоживущей транзакцией. При бестолковом применении это разрушительно скажется на производительности, но может решить проблему единого срока таймаута транзакций для конкретной машины. Можно использовать BYOT для запуска транзакционных объектов с уровнем изоляции ниже serializable, еще одна трудновыполнимая под MTS задача.

Вывод

Все навороты в контекстах, апартаментах и активностях – явная примета того, что эпоха революционности COM осталась позади. Сейчас идет нормальный эволюционный процесс, имеющий огромное количество стадий. Для коммерческих фирм типа Microsoft характерно превозносить любое новое свойство, введенное в продукт или систему. Стоит появиться изменению, развивающему имевшуюся модель, как на старую модель немедленно обрушиваются стрелы критики. Эти стрелы необходимы для популяризации новинок. Нужно помнить, что любая новинка когда-нибудь обязательно превратится в legacy-технологию. Главное – угадать, какое из свойств новой технологии устареет через год. Для этого следует понимать основы технологии и, желательно, помнить историю их возникновения. Эта статья не столько рассказывает о текущем состоянии дел, сколько служит историческим экскурсом и практическим пособием. Попробуйте вернуться к этой статье, когда продвинетесь в изучении COM довольно далеко. Я уверен, вы найдете в ней немало нового и интересного.


Впервые статья была опубликована в журнале <Технология Клиент-Сервер>.
Эту и множество других статей по программированию, разработке БД, многоуровневым технологиям (COM, CORBA, .Net, J2EE) и CASE-средствам вы можете найти на сайте www.optim.su и на страницах журнала.
    Сообщений 10    Оценка 115        Оценить