Понимание потоковых моделей в COM при программировании на Delphi - Объект MTA и защита данных
ОГЛАВЛЕНИЕ
Правило #9:
Объект MTA не может делать никаких предположений о том, когда и какими потоками он будет вызван. Следовательно, Ваш объект MTA должен обеспечивать защиту как глобальных, так и специфических для экземпляра данных, которые потенциально могут быть разрушены в результате одновременного доступа к объектам различных потоков.
В качестве простого примера рассмотрим сервер MTA, содержащий объект Object1, имеющий метод Method1. Если клиент создает объект Object1 и производит два вызова метода Method1, то уже для второго вызова может случиться так, что он придет из потока, отличного от того, в котором был выполнен первый вызов. Жуть, не так ли? Т.е. Вы видите, что отсутствие последовательности вызовов обеспечивает максимум производительности, но отсутствие последовательности же вызовов означает больше работы для обеспечения целостности объекта. Из Правил #8 и #9 очень легко заметить, что поддержка модели MTA практически применима лишь в ситуации, когда Ваши объекты являются чисто служебными объектами, т.е. они не работают интенсивно с какими-то глобальными или специфичными для экземпляра данными, или когда большинство вызываемых/используемых методов Вашего объекта не требует какого-либо кода для защиты данных от разрушения при взаимодействии нескольких потоков.
Я хочу сказать, что выбор модели MTA должен быть результатом осознанного решения, основанного на том, как будут использоваться Ваши объекты. Если Ваши объекты не могут обслуживать одновременный доступ из нескольких потоков к одному экземпляру объекта на достаточном уровне для использования выгоды в скорости, предоставляемой моделью MTA, а Вы думаете об использовании модели MTA, то это значит, что Вы либо не знаете, что Вы делаете, либо имеете уйму времени на занятия бессмыслицей. Конечно, Вы всегда можете взять объект STA, просмотреть все методы и защитить от совместного доступа все поля и глобальные переменные критическими секциями и тогда переключить его в режим MTA. Но 1) Ваш объект MTA будет работать так же, как и в "старом" варианте STA и 2) Вы вынуждены будете написать существенно больше кода.
Другая важная вещь, которую Вы должны знать об MTA заключается в том, что COM не будет выстраивать последовательно вызовы к потоку MTA. Это означает, что нет скрытого окна, как в случае с STA, и, следовательно, Ваш MTA не нуждается в цикле обработки сообщений для своег функционирования. В действительности внешнему серверу MTA, содержащему только объекты MTA, нет необходимости иметь цикл обработки сообщений ни в каком из потоков, даже в главном. Что, однако, необходимо, так это сохранить себя в рабочем состоянии, если имеется хотя бы одна ссылка к какому-либо из экземпляров объекта, т.е. значение блокирующего счетчика больше нуля, и способ сигнализировать главному потоку о том, что счетчик стал равен нулю. Вне зависимости от того, какой из способов Вы предпочтете для реализации этого, всегда необходимо помнить, что поток MTA не нуждается в цикле обработки сообщений.
Теперь, когда мы крепко усвоили основы MTA, давайте посмотрим, как клиентское и серверное приложения взаимодействуют друг с другом в условиях модели MTA. Процесс создания объектов MTA во внешнем сервере намног проще, чем в модели STA. Когда бы клиент не затребовал у внешнего сервера MTA создание объекта, у сервера нет необходимости порождать новый поток и делегировать создание этого объекта этому потоку, как это мы делали для сервера STA. Причина этого в том, что COM создает пул потоков, который будет использоваться для управления вызовами Ваших объектов MTA. Этот пул потоков, иногда называемый пулом потоков RPC (удаленный вызов процедуры - Remote Procedure Call - низкоуровневая реализация вызовов COM времени исполнения для межпроцессного взаимодействия), внутренне создается и управляется COM и не зависит от того, как реализован Ваш сервер MTA. Другими словами главному потоку Вашего сервера MTA достаточно только вызвать при запуске CoInitializeEx (NIL, COINIT_MULTITHREADED) и нет нужды вмешиваться в процесс "обживания" объекта, что означает, что Вам достаточно правильно создать объекты в главном потоке, в который входит MTA.
Теперь, хотя Ваш объект и был создан в главном потоке Вашего сервера, нет необходимости, чтобы Ваш объект принимал поступающие вызовы в этом потоке. COM будет использовать внутренний пул RPC при распределении запросов клиента к Вашему объекту. Это и есть та причина, почему объекты MTA не могут делать никаких предположений о том, какой поток сейчас производит вызов какого-то метода. Внутренний сервер MTA сообщает COM, что его объекты могут жить в MTA, обозначив строковый параметр в системном реестре как "ThreadingModel=Free." Таким образом, внутренний сервер, содержащий объект Object1, поддерживающий MTA должен содержать следующий входы системного реестра:
[HKCR\CLSID\<Object1 Class Id>\InprocServer32](Default)=Server.dll
[HKCR\CLSID\<Object1 Class Id>\InprocServer32]ThreadingModel=Free
Назначив параметру ThreadingModel значение Free, сервер, в основном, сообщает COM, что когда бы клиентский поток MTA не захотел создать объект Object1, COM может двигаться вперед и создавать экземпляр Object1 прямо в клиентском MTA. Под "прямо" я подразумеваю, что клиентский поток получит непосредственно указатель на интерфейс (а не заместителя) объекта Object1. Все остальные потоки, входящие в MTA позже, могут свободно осуществлять доступ к объекту Object1, как будто они сами создали объект Object1, т.е. они все управляют объектом Object1, используя прямые ссылки. Это означает, что все клиентские потоки в MTA гарантируют максимальную производительность объекта Object1 при использовании модели MTA.
Заметьте, что я сказал максимум производительности потому, что все потоки могут одновременно делать вызовы к методам объекта Object1, а объект Object1 может счастливо обслуживать все вызовы одновременно в противоположность последовательному процессу (который делает "одновременное" обслуживание нескольких клиентов существенно медленнее), производимому COM, если бы Object1 жил в STA. К этому моменту мы уже посмотрели на STA, изучили MTA и узнали, как клиентское и серверное приложения взаимодействуют в условиях моделей STA и MTA. Но прежде, чем Вы назовете себя гуру потоковых моделей COM, имеется еще одна штука, которую необходимо принять во внимание: как клиенты и сервера различных потоковых моделей взаимодействуют между собой.
Если Вы вспомните, Правило #4 устанавливает, что COM работает аккуратно, гарантируя, что два приложения с несовместимыми потоковыми моделями могут и будут правильно взаимодействовать друг с другом. Что я Вам еще не сказал, так это как в действительности COM устраивает это. Для того, чтобы понять весь процесс целиком, давайте пройдем через несколько сценариев комбинаций потоковых моделей и посмотрим, как COM поступает при каждом из этих сценариев.
Сценарий #1: Клиент STA и однопотоковый сервер Внутренний сервер. Клиентский поток STA, создающий объект в однопотоковом сервере, принимает прямое подключение к объекту, если этот поток живет в главном STA клиента. В противном случае COM создает объект в главном STA клиента и получает прокси (заместителя) к требуемому потоку. Почему COM поступает так? Ответ простой: однопотоковый сервер сообщил COM, что он может обслуживать поступающие вызовы к своим объектам только в одном потоке. COM заметит это и несомненно выполнит этот однопотоковый запрос, не "потревожив" сервер. Следовательно, любой многопотоковый клиент, желающий сообщить что-либо этому серверу, будет способен сделать это только потому, что COM заставить выполнить этот запрос в одном потоке STA. Для клиента STA COM выберет главный STA в качестве единственного потока STA. Внешний сервер. Все клиентские потоки STA будут пользоваться заместителями, маршалируемыми и обслуживаемыми главным (единственным) STA внешнего сервера.
Сценарий #2: Клиент STA и сервер MTA Внутренний сервер. Клиентский поток STA, создающий объект из сервера MTA, получит прокси (заместителя) к этому объекту, маршалированного из созданного COM MTA в клиентском приложении. На первый взгляд это кажется странным, так как по определению объект MTA должен иметь возможность прямого доступа вне зависимости от того, какой поток создан, или откуда к нему производится доступ. Другими словами, почему бы COM не создавать объект непосредственно в STA запрашивающего клиента? Давайте попробуем понять, что произойдет, если COM создаст экземпляр объекта непосредственно в клиентском STA. Если COM создает объект MTA непосредственно в клиенте STA, то с точки зрения клиента, объект живет в этом STA, но с точки зрения сервера объект в действительности живет в MTA, т.е. он выглядит так, как будто он может осуществлять все вызовы из любого потока в любое время. Теперь, если клиент пытается передать интерфейс обратного вызова методу этого объекта MTA (очевидно, что этот интерфейс нужен объекту, расположенному в клиенте, а этот объект поддерживает только STA, так как клиент работает в модели STA) и сервер пытается осуществить обратный вызов через этот интерфейс, то у сервера нет способа узнать, что этот интерфейс не в состоянии обслуживать одновременные вызовы из нескольких потоков сервера (который размещается в MTA). Другими словами клиентский объект, реализующий интерфейс обратного вызова может "задохнуться", если сервер начнет производить одновременные вызовы из разных потоков.
Следовательно, создавая объект в созданном COM MTA и передавая прокси (заместителя) обратно к затребовавшему его STA, любые обратные вызовы, исходящие от сервера, будут производиться этим MTA и выстраиваться последовательно через прокси в STA, который содержит объект, обслуживающий обратный вызов. Внешний сервер. Все клиентские потоки STA, которым необходимо использование прокси, маршалируются и обслуживаются в MTA внешнего сервера.
Сценарий #3: Клиент MTA и однопотоковый сервер Внутренний сервер. Клиентский поток MTA, создающий объект из однопотокового сервера, будет принимать прокси к этому объекту, маршалированный созданным COM главным STA в клиенте (полагая, что клиент еще не создан главным STA). Очевидно, COM не может допускать прямого создания объектов однопотокового сервера в MTA, так как он не сможет пережить одновременные вызовы из потоков MTA. Внешний сервер. Все клиентские потоки MTA будут пользоваться прокси (заместителями), маршалированными и обслуживаемыми главным (единственным) STA внешнего сервера.
Сценарий #4: Клиент MTA и сервер STA Внутренний сервер. Клиентский поток MTA, создающий объект из сервера STA, получит прокси (заместителя) к этому объекту, маршалированного, созданным COM STA в клиентском приложении. Только это имеет смысл, так как сервер сказал COM, что он может поддерживать только STA и поэтому нет способа, чтобы COM прямо создавал объект в MTA, в котором другие потоки MTA преспокойно "завалят" его! Таким образом, если он живет в STA, любые вызовы, производимые потоками MTA, будут выстраиваться последовательно к STA, который, по соглашению, как раз и является тем, с чем может работать сервер. Внешний сервер. Все клиентские потоки MTA будут пользоваться прокси, маршалированными и обслуживаемыми в каждом соответствующем STA внешнего сервера.
Сценарий #5: Однопотоковый клиент и сервер STA Внутренний сервер. В этом случае, очевидно, имеется только один главный поток в клиентском приложении, в котором COM будет непосредственно создавать все объекты сервера STA. Внешний сервер. Клиентский главный поток STA будет пользоваться прокси, маршалированным и обслуживаемым в каждом соответствующем STA внешнего сервера.
Сценарий #6: Однопотоковый клиент и сервер MTA Внутренний вервер. Этот сценарий является недоделанным вариантом сценария #2, в котором имеется только один STA в клиенте, главный STA. Таким образом, по тем же причинам, что и при сценарии #2, COM будет создавать объект в созданном COM MTA и возвращать прокси к главному потоку STA. Внешний сервер. Клиентский главный поток будет пользоваться прокси, маршалируемым и обслуживаемым в MTA внешнего сервера. Ну вот мы и рассмотрели все возможные несовместимые комбинации потоковых моделей клиентов и серверов. Мы увидели, как COM предоставляет возможность этим клиентам и серверам работать совместно. Теперь я бы хотел рассказать о необычайно интересном вопросе смешанного использования потоковых моделей.
С появлением STA и MTA иногда стала появляться необходимость взаимодействия клиентов и серверов с потоками STA в одних местах и с потоками MTA в других местах внутри одного приложения. Обычно такая потребность появляется по бизнес-причинам, появляющимся при тщательном изучении того, как Ваши клиентское и серверное приложения будут взаимодействовать друг с другом. Например, Ваш сервер может нуждаться в использовании некоторых объектов для решения задач реального времени, в то время как другие не должны (или не могут) работать в режиме "производительности реального времени". В этом случае логично иметь объекты "реального времени", создаваемые в MTA, где они могут реализовать максимальную производительность, и, в то же время, иметь остальные объекты, обслуживаемые в одном или многих STA. То, что я должен описать здесь, называется Смешанная потоковая модель (Mixed Threading Model), обозначая тем самым, что Ваше приложение пользуется комбинацией (смесью) различных потоковых моделей для своих объектов.
В действительности в смешанной модели нет ничего нового. Клиентское приложение может, например, создавать целый букет рабочих потоков, живущих в MTA, в то время как другая группа потоков STA обслуживает какие-то другие потребности. Серверное приложение также может работать аналогично, т.е. организовывать гроздь объектов в MTA для получения максимальной производительности и, в то же время, порождать кучу потоков STA для других объектов. Мне нет никакой необходимости демонстрировать, как клиент или сервер собирается создавать STA и MTA в рамках единственного процесса, так как Вы уже знакомы с технологией как это делается. Необходимо только создать букет потоков, каждый из которых входит либо в STA, либо в MTA и, бум!, Вы получили смешанную потоковую модель, работающую в Вашем приложении. Об этой смешанной модели важно знать то, что она существует и может оказаться очень удобной для решения каких-то проблем, которые могут встретиться Вам при создании Вами клиентских и серверных приложений. Поддержка смешанной потоковой модели для внешних серверов заключается просто в создании букета потоков и явным указанием для каждого потока его STA или MTA. Для внутренних серверов, однако, мы можем ожидать, что COM полагается на строковый параметр ThreadingModel, как и в случае однопотокового STA. Сервером объектов может быть использован строковый параметр "ThreadingModel=Both" для указания, что COM может свободно создавать этот объект в STA или в MTA, т.е. он поддерживает как STA, так и MTA. Но как COM узнает, должен он создавать объект в STA или в MTA? Как я уже говорил ранее, внутренний сервер обычно рассчитывает, что клиентское приложение явно создает потоки STA и MTA, содержащие серверные объекты.
В случае, когда "ThreadingModel=Both", подразделение клиентского потока прямо определяет, где COM будет создавать этот объект. Другими словами, если клиентский поток STA создает объект - COM явно создает его в STA. Если клиентский поток MTA создает объект - COM однозначно создаст его в MTA. Если Вы зашли так далеко и поняли все то, о чем я говорил в этой статье - поздравляю Вас! Смотрите, я же говорил, что потоки в COM - это просто. Вы просто должны затратить немного времени, чтобы не спеша понять много нового, но в конце концов потоки они и есть потоки, это просто новое лицо, появляющееся при интеграции с COM. Я надеюсь, что Вы наслаждались чтением этой статьи так же, как я наслаждался написанием этой статьи (и потоковыми моделями). На этом все. Скоро ждите еще много интересного !
E-mail: Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.