Продолжается подписка на наши издания! Вы не забыли подписаться?

<назад     <<в начало статьи

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 компоненты. Такие компоненты называются конфигурированными.

Таблица 2   Опции конфигурации.

Атрибут

Значения

Уровень

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

Вкл./Выкл.

Метод

 

 

Таблица 3   Атрибуты: Приложения

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

Таблицы 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 сделает модель программирования более сложной, так как ссылки на prox y придется рассматривать не так, как прямые ссылки на реальные объекты. Подведем итог, не используйте объектные ссылки в разных контекстах без маршалинга.

Рис. 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-программирования, возможно, спросят – не заменяют ли контексты апартаменты. Ответ – и да, и нет. Та часть, которая «да», говорит, что контексты заменяют апартаменты как самую дальнюю внутреннюю область действия объекта. Объектные ссылки теперь контекстно-зависимы — не только апартаментно-зависимы — а aeyrwbz 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 предпочтителен в большинстве случаев.

Таблица 4.

CoCreateInstance
вызван из:

Класс регистрирован как ThreadingModel

Apartment

Both

Free

отсутствует

Neutral

Новый объект активирован в

STA-поток

в текущем STA

Текущий апартамент

MTA

Main STA

TNA

TNA
(STA-поток)

MTA

в отдельном STA

TNA
(MTA-поток)

Вас может заинтересовать, как изменяется интерпретация установок в случае нового типа апартамента. Таблица 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-активацию или транзакции, требуют активности.

Таблица 5 .

Установки синхронизации для нового класса

Имеет активность?

Входит в активность создателя?

NOT_SUPPORTED

Никогда

Никогда

SUPPORTED

Если создатель входит в активность

Если создатель входит в активность

REQUIRED

Всегда

Если создатель входит в активность

REQUIRES_NEW

Всегда

Никогда

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 дает объект, который может стать корнем потока транзакций, а может и не стать.

Таблица 6.

Установки транзакций для нового класса

Принадлежит потоку транзакций?

Разделяет поток транзакций создателя?

Корень потока?

NOT_SUPPORTED

Никогда

Никогда

Никогда

SUPPORTED

Как и создатель

Как и создатель

Никогда

REQUIRED

Всегда

Как и создатель

Как и создатель

REQUIRES_NEW

Всегда

Никогда

Всегда

Наконец, заметьте, что для любых двух потоков транзакций результат любых их транзакций совершенно независим. Вопреки тому, во что вы интуитивно верите, вторая транзакция, вытекающая из 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 довольно далеко. Я уверен, вы найдете в ней немало нового и интересного.

Литература:

  1. Don Box, «Windows 2000 Brings Significant Refinements to the COM(+) Programming Model», MSDN Magazine, May 2000
  2. Rama Krishna, Getting a Feel of COM Threading Models, www.codeguru.com
  3. Microsoft Developer Network, 2000

Copyright © 1994-2016 ООО "К-Пресс"