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

Понимание подразделений COM

Автор: Jeff Prosise
Перевод: Павел Блудов
Источник: CodeGuru::Into the IUnknown
Опубликовано: 22.02.2001
Исправлено: 13.03.2005
Версия текста: 1.4.5

COM реализует механизм параллельности, способный перехватывать и упорядочивать параллельные вызовы методов для объектов, которые были рассчитаны на обработку только одного вызова в один момент времени. Этот механизм базируется на понятии абстрактных границ, называемых подразделениями (apartments). На долю ошибок, связанных с недопониманием сути подразделений приходится примерно 40% от общего количества ошибок связанных с COM. Этот недостаток знаний не должен удивлять, т.к. подразделения являются не только одной из самых сложных областей COM, но и хуже всего документированы. Разработчики из Microsoft ввели подразделения COM в Windows NT 3.51 из хороших побуждений, но при этом они заложили минное поле для неосторожных программистов. Играйте по правилам и вы сможете не наступать на мины. Однако тяжело придерживаться правил, когда вы их не знаете.

Основы подразделений

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

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

ПРИМЕЧАНИЕ
Подразделения не выходят за границы процесса, так что если объект и его клиент находятся в разных процессах, то они принадлежат также и разным подразделениям.

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

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


Рисунок 1: Вызовы объектов из других подразделений маршалятся, даже если объект и клиент принадлежат одному и тому же процессу.

Windows NT 4.0 (и Windows9x после установки DCOM'а) поддерживает два типа подразделений; Windows 2000 поддерживает три:

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

ПРИМЕЧАНИЕ
STA-шные объекты никогда не обрабатывают более одного вызова в один момент времени.

COM использует STA для упорядочивания вызовов из непотокобезопасных объектов. Если вы не укажете специально, что объект потокобезопасный, COM поместит его в STA и объект не будет страдать из-за асинхронных вызовов из разных потоков.

Один из тонких моментов STA состоит в том, как COM перемещает вызовы для STA-шных объектов в STA-шный поток. Когда создается STA, COM одновременно создает невидимое окно для этого подразделения. Оконная процедура (windowproc) этого окна знает как обрабатывать специальные сообщения представляющие вызовы методов. Когда вызов метода, предназначенный для STA появляется из COM-ового RPC канала, COM посылает сообщение представляющее этот вызов STA-шному окну. Когда поток в STA получает это сообщение он отправляет его невидимому окну и оконная процедура этого окна передает этот вызов в заглушку. Заглушка, в свою очередь, выполняет прямой вызов объекта. Поскольку поток принимает, распределяет и обрабатывает только одно сообщение за раз, назначение объекту STA реализует грубый (но эффективный) механизм синхронизации вызовов. Как показано на Рисунке 2, если n вызовов были адресованы STA-шному объекту одновременно - параллельно, то все эти вызовы будут поставлены в очередь и достигнут объекта по одному за раз - последовательно.


Рисунок 2: Вызовы, попадающие в STA, конвертируются в сообщения и посылаются в очередь сообщений. Сообщения, полученные из очереди, переводятся обратно в вызовы методов по одному за раз в потоке, принадлежащем STA.

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

ПРИМЕЧАНИЕ
По умолчанию любой вызов, попадающий в STA, блокируется если STA-шный поток ожидает возврата отправленного вызова, а входящий и выходящий вызовы не являются частью одной цепочки (например, относятся к разным подразделениям).

Можно, тем не менее, вмешиваться в этот процесс путем написания фильтров сообщений, но это выходит за рамки данной статьи.

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

Чем MTA отличаются от STA? Помимо того, что каждый процесс ограничен одним MTA, и что данный MTA может содержать любое число потоков, MTA не имеет скрытого окна и очереди сообщений. Внешние вызовы объекта, находящегося в MTA, передаются произвольно выбранным потокам из пула потоков RPC и не синхронизируются (см. рис. 3).

ПРИМЕЧАНИЕ
Объекты помещенные в MTA должны быть потокобезопасными, поскольку при отсутствии внешнего механизма, гарантирующего очередность вызовов, объекты будут параллельно принимать вызовы из разных RPC потоков.


Рисунок 3: Вызовы попадающие в MTA доставляются в RPC потоки но не становятся в очередь.

Когда вызов покидает MTA, COM не делает ничего особенного. Вызывающий поток может быть запросто заблокирован и если случится обратный вызов, он будет обработан в другом RPC потоке.

В Windows 2000 появился третий тип подразделений: потоконезависимые подразделения, или NTA (часто встречается TNA). COM ограничивает каждый процесс одним NTA. Потоки никогда не назначаются NTA; в NTA находятся только объекты. Важной особенностью NTA является то что вызовы NTA-шных объекты не подвергаются подмене потока.

ПРИМЕЧАНИЕ
Когда вызов из STA или MTA приходит в NTA того же самого процесса, вызывающий поток временно выходит из своего подразделения и выполняется уже в NTA.

В отличии от STA- и MTA-шных объектов, которые всегда подвергаются подмене потока при вызове из других подразделений. Переключение потоков связано с большими накладными расходами при маршалинге и демаршалинге вызовов между подразделениями. Отказ от переключения потоков сильно увеличивает производительность. Следовательно, можно считать NTA оптимизацией позволяющей осуществлять вызовы между подразделениями более эффективно. В дополнение, Windows 2000 поддерживает внешний механизм синхронизации основанный на возможности непосредственно указать, какие вызовы NTA-шного объекта должны быть синхронизованы. Такая синхронизация более эффективна чем основанная на сообщениях и может быть заявлена (или не заявлена) при работе любого объекта с любым другим.

Как потокам назначают подразделения

Одно из главных правил COM программирования состоит в том, что каждый поток, использующий COM, должен ее инициализировать путем вызова CoInitialize() или CoInitializeEx(). Когда поток вызывает одну из этих функций, он собственно и помещается в подразделение. Тип подразделения зависит от того, какая функция была вызвана и каким образом. Если поток вызывает CoInitialize(), COM создает новый STA и помещает в него поток:

::CoInitialize(NULL); // STA

Если поток вызывает CoInitializeEx() и передает в качестве второго параметра COINIT_APARTMENTTHREADED, то он тоже оказывается в STA:

::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // STA

Вызов CoInitializeEx() с COINIT_MULTITHREADED помещает поток в единственный MTA процесса:

::CoInitializeEx(NULL, COINIT_MULTITHREADED); // MTA

В большинстве случаев, конфигурация подразделений процесса зависит от того как потоки этого процесса вызывают CoInitialize[Ex]. Есть случаи, в которых COM может создать новое подразделение вне вызова CoInitialize[Ex], но пока мы не будем забивать мозги, рассматривая такие обстоятельства.

В качестве примера, предположим что новый процесс только что начался и поток этого процесса (поток 1) вызывает CoInitialize():

::CoInitialize(NULL); // Thread 1

Кроме того, предположим что поток 1 запускает потоки 2, 3, 4, и 5, и эти потоки инициализируют COM следующим образом:

::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // Thread 2
	
::CoInitializeEx(NULL, COINIT_MULTITHREADED); // Thread 3
::CoInitializeEx(NULL, COINIT_MULTITHREADED); // Thread 4
::CoInitialize(NULL); // Thread 5

Рисунок 4 показывает конечную конфигурацию подразделений. Потоки 1, 2, и 5 принадлежат трем различным STA, потому что именно так они вызывали CoInitialize() и CoInitializeEx(). Они помещены в различные STA, поскольку STA ограничены одним потоком на каждого. Потоки 3 и 4, с другой стороны, попали в MTA процесса. COM никогда не создает более одного MTA в одном процессе, но помещает в него любое количество MTA-шных потоков.


Рисунок 4: Процесс с пятью потоками, распределенными между тремя STA и одним MTA.

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

Как внутрипроцессным объектам назначаются подразделения

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

COM определяет в каком подразделении должен быть создан внутрипроцессный объект путем считывания поля "ThreadingModel" из ключа реестра посвященного объекту. Следующие поля реестра, показанные здесь в формате REGEDIT'а, идентифицируют объект, чей CLSID - 99999999-0000-0000-0000-111111111111, DLL - MyServer.dll, и ThreadingModel - Apartment:

[HKEY_CLASSES_ROOT\CLSID\{99999999-0000-0000-0000-111111111111}]
@="My Object"
[HKEY_CLASSES_ROOT\CLSID\{99999999-0000-0000-0000-111111111111}\InprocServer32]
@="C:\\COM Servers\\MyServer.dll"
"ThreadingModel"=<Потоковая модель>

<Потоковая модель> - это одно из четырех значений, поддерживаемых Windows NT 4.0, или пяти, поддерживаемых Windows 2000:

Потоковая модель Тип подразделения
None Главное STA
Apartment Любое STA
Free MTA
Both STA или MTA
Neutral NTA (только Win2k)

Столбец "Тип подразделения" показывает, как COM использует объект в зависимости от значения "Потоковая модель". Например, COM ограничивает объект, у которого не задано значение ThreadingModel ("ThreadingModel=None"), главным STA процесса. ThreadingModel=Apartment позволяет объекту быть созданным в любом STA (не только в главном STA), в то время как ThreadingModel=Free ограничивает объект MTA, и ThreadingModel=Neutral ограничивает NTA. Только ThreadingModel=Both позволяет COM делать выбор, каким образом создавать объект: в STA или MTA.

ПРИМЕЧАНИЕ
COM старается помещать внутрипроцессные объекты в то же подразделение, что и у создающих их потоков.

Например, если STA поток создает объект, помеченный как ThreadingModel=Apartment, то COM создаст объект в STA создающего его потока. Если MTA поток создает ThreadingModel=Free объект, COM поместит его в MTA рядом с создающим потоком. Иногда все же, COM не может поместить объект в создающее его подразделение. Если например, STA поток создает объект, помеченный как ThreadingModel=Free, то объект будет создан в MTA процесса, и создающий поток будет общаться с объектом через представителя и заглушку. Аналогично, если MTA поток создает объект с ThreadingModel=None или ThreadingModel=Apartment, то вызовы из этого потока будут отмаршалены из MTA в STA объекта. Следующая таблица показывает, что получается, когда поток из STA или MTA создает объект с известным ей значением ThreadingModel (или без ThreadingModel):

None Apartment Free Both Neutral
STA Главные STA Создающие STA MTA Создающие STA NTA
MTA Главные STA STA MTA MTA NTA

Почему ThreadingModel=None ограничивает объект только главным STA процесса? Потому что только так COM может быть уверена, что несколько экземпляров объекта, ничего не знающего о потокобезопасности, могут выполняться безопасно. Представьте, что два ThreadingModel=None объекта созданы из одной и той же DLL. Если объекты изменяют какую-либо из глобальных переменных в этой DLL (и они это наверняка делают), то COM должна выполнять все вызовы этих объектов в одном и том же потоке, иначе возможно, что они попытаются читать или писать в одну и ту же переменную одновременно. Ограничение, связанное с главным STA - это способ, который использует COM, чтобы гарантированно помещать такие объекты в один поток.

Хоть это и не очевидно на первый взгляд, но потоковая модель, которую вы выберете, серьезно повлияет на код, который вы будете писать в дальнейшем. Например, объект, помеченный ThreadingModel=Free или ThreadingModel=Both, должен быть полностью потокобезопасным, так как вызовы методов MTA-шных объектов не синхронизованы извне. Даже ThreadingModel=Apartment объекты должны быть "немного потокобезопасными", поскольку ThreadingModel=Apartment не мешает нескольким объектам созданным в одной DLL ссориться из-за разделяемых данных (в случае, если они попадут в разные STA). Мы рассмотрим это подробнее в дальнейшем.

Как внепроцессным объектам назначаются подразделения

Внепроцесные объекты не имеют поля ThreadingModel в реестре, поскольку COM использует совершенно другой алгоритм для назначения подразделений внепроцессным объектам. Коротко говоря, COM помещает внепроцессные объекты в то же подразделение, что и у потока серверного процесса, их создавшего. Большинство внепроцессных (EXE) COM серверов начинают с вызова CoInitialize() или CoInitializeEx(), чтобы поместить их основной поток в STA. Затем они создают объекты классов объектов (Class Factory) и регистрируют их при помощи CoRegisterClassObject(). Когда к серверу, инициализированному таким образом, приходит запрос на активацию объекта, этот запрос обрабатывается в главном STA процесса. Как следствие, созданные объекты тоже оказываются в STA

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

ПРИМЕЧАНИЕ
Ответственность за выбор подразделения для внепроцессных объектов целиком лежит на программисте, реализующем внепроцессный сервер.

ПРИМЕЧАНИЕ
Клиенты никогда не получают прямых указателей на интерфейсы внепроцессных объектов, поскольку они принадлежат разным процессам и соответственно, разным адресным пространствам.

Как правило, подразделение, в котором размещен поток, вызывающий CoRegisterClassObject() - это то же подразделение, в которых живут все созданные сервером объекты. Бывают и исключения: EXE COM сервера, написанные с ATL-евыми CComAutoThreadModule и CComClassFactoryAutoThread классами, могут создавать несколько STA в серверном процессе и распределять объекты между ними. Однако этот случай - большая редкость среди EXE COM серверов, существующих на сегодняшний день, и может быть расценен не более чем исключение из правила.

Как создавать работоспособные COM клиенты

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

Правило 1: Клиентские потоки должны вызывать CoInitialize[Ex]

Прежде чем поток сделает что-либо, что использует COM, он должен вызвать CoInitialize() или CoInitializeEx() чтобы проинициализировать COM. Если клиентское приложение имеет 20 потоков, и 10 из них используют COM, то каждый из этих 10 потоков должен вызвать CoInitialize() или CoInitializeEx(). Внутри этих API функций, вызывающий поток приписывается к подразделению. Если поток не был приписан к подразделению, то COM не в состоянии применить законы параллелизма COM для потока. Не забывайте также, что поток, вызвавший CoInitialize() или CoInitializeEx() успешно, должен вызвать CoUninitialize() прежде чем он завершит свою работу. Иначе ресурсы, выделенные в CoInitialize[Ex] не будут освобождены до тех пор, пока не завершится сам процесс.

Кажется, что очень легко не забыть вызвать CoInitialize() или CoInitializeEx() до использования COM. Это один вызов функции. Поразительно, как часто это простое правило бывает нарушено. Чаще всего ошибка проявляется в виде неудавшихся вызовов CoCreateInstance() и ей подобных COM API функций. Но очень часто ошибки появляются много позже, и не имеют очевидной связи с несостоявшейся инициализацией COM.

Забавно, что одна из причин, по которой разработчики не используют CoInitialize[Ex] заключается в том, что Майкрософт советует этого не делать. В MSDN имеются документы, сообщающие программистам что COM клиенты могут иногда избегать вызова этих функций. В них сказано, что может произойти нарушение доступа. На самом деле наоборот, вызов метода Release() из потоков, не инициализировавших COM может вызвать нарушение доступа.

Помните: никогда не помешает вызвать CoInitialize[Ex], и это должно быть обязательным для тех потоков, которые вызывают API функции COM и так или иначе инициализируют COM объекты.

Правило 2: STA Потокам нужен цикл обработки сообщений.

Правило 2 не столь очевидно, если только вы не знаете как устроены однопоточные подразделения. Когда клиент вызывает метод STA-шного объекта, вызов перемещается в поток, который находится в STA. COM выполняет доставку вызова путем помещения сообщения в очередь сообщений невидимого окна, созданного STA. Что же произойдет, если поток в STA не получит и не сможет обработать эти сообщения? Вызов уйдет в RPC канал и никогда не вернется. Он навсегда останется в очереди сообщений STA-шного окна.

Когда поток вызывает CoInitialize(), или CoInitializeEx() с параметром COINIT_APARTMENTTHREADED, или MFC-ную функцию AfxOleInit(), он оказывается в STA. Если позже в этом потоке будут созданы какие-то COM объекты, они не смогут принимать вызовы методов из других подразделений, если STA-шный поток не осуществляет подкачку сообщений (message pump). Подкачка сообщений может быть такая же простая как эта:

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

Избегайте помещать поток в STA, если он не удовлетворяет этому простому требованию. Очень распространен случай, когда приложение MFC создает рабочий поток (MFC-шние рабочие потоки, по определению, не осуществляют подкачки сообщений) и этот поток вызывает AfxOleInit(), которая помещает его в STA. Вы не встретите никаких проблем если это STA не содержит никаких объектов, либо содержит, но те не имеют клиентов в других подразделениях. Но если это STA заведет объект, который выдает указатели на интерфейсы в другие подразделения, то никакой вызов, отправленный через эти интерфейсные указатели, не вернется.

Правило 3: Никогда не передавайте между подразделениями прямые, немаршаленные указатели на интерфейсы.

Предположим, вы пишете COM клиента с двумя потоками. Оба потока вызывают CoInitialize() чтобы попасть в STA, и один из потоков -- поток A -- вызывает CoCreateInstance() для создания COM объекта. Поток A хочет передать указатель на интерфейс, полученный от вызова CoCreateInstance(), потоку B. Для этого поток A помещает указатель в глобальную переменную и дает знать потоку B, что указатель получен. Поток B считывает указатель на интерфейс из глобальной переменной и производит несколько вызовов объекта через этот указатель. Подождите, а все ли здесь в порядке?

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

Поток A может безопасно разделять указатель на интерфейс с потоком B, если он отмаршалит этот указатель. Вот два основных способа которыми COM клиент может отмаршалить интерфейс в другое подразделение:

Бывают ли случаи, когда нет необходимости в маршалении указателей на интерфейсы, которые нужно передавать между потоками? Да. А именно, если два потока принадлежат одному и тому же подразделению, что может быть только в том случае, если это подразделение -- MTA. В этом случае они могут передавать прямые, немаршаленные указатели на интерфейсы. Но если вы сомневаетесь, то лучше маршалить. Никогда не помешает вызвать CoMarshalInterThreadInterfaceInStream() и CoGetInterfaceAndReleaseStream() или воспользоваться GIT, поскольку COM не будет маршалить указатель если в этом нет необходимости.

Как создавать работающие COM сервера

Вот примерный набор правил, которым необходимо следовать чтобы правильно создавать COM сервера. Они описанны ниже.

Правило 1: Защищайте разделяемые данные в ThreadingModel=Apartment объектах

Одно из наиболее часто встречающихся недоразумений связанных с COM программированием заключается в том, что программист, помечавший объект как ThreadingModel=Apartment может не волноваться насчет потокобезопасности. Фигушки! Когда вы регистрируете внутрипроцессный объект с ThreadingModel=Apartment, вы неявно сообщаете COM что разные экземпляры этого объекта (и другие объекты из этой DLL) имеют доступ к разделяемым данным в потокобезопасной манере. Это означает, что вы используете критические секции или иные механизмы синхронизации для того чтобы только один поток мог использовать данные в одно и то же время. Данные, разделяемые между экземплярами объекта обычно имеют три формы:

Почему синхронизация потоков так важна для ThreadingModel=Apartment объектов? Представьте случай когда два объекта -- объект A и объект B -- были созданы из одной и той же DLL. Предположим, что оба объекта читают и пишут в глобальную переменную описанную в DLL. Поскольку оба объекта помечены как ThreadingModel=Apartment, они могут быть созданы в различных STA и выполняться, таким образом, в двух разных потоках. Но глобальная переменная, к которой оба они имеют доступ, существует в этом процессе в единственном экземпляре. Если вызовы объектов A и B происходят примерно в одно и то же время, и если объект A пишет в глобальную переменную, которую в это же время читает поток B (или наоборот), переменная может получить искаженное значение - если только вы не будете выполнять эти действия последовательно. Если вы не позаботитесь о механизме синхронизации, то в большинстве случаев вы не столкнетесь ни с какими проблемами. Но в конечном счете эти два потока встретятся в одно и то же время в одном и том же месте и наверняка поссорятся из-за разделяемых данных, и никто не в состоянии предположить чем это может закончиться.

Могут ли COM объекты работать с разделяемыми данными не заботясь о подобной синхронизации? Да, в следующих случаях:

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

Правило 2: Объекты помеченные как ThreadingModel=Free или ThreadingModel=Both должны быть целиком потокобезопасными.

Когда вы регистрируете ThreadingModel=Free или ThreadingModel=Both объект, он будет (Free) или может быть (Both) помещен в MTA. Помните, что COM не выстраивает в очередь вызовы MTA-шных объектов. Так что, если только вы не уверены без тени сомнения, что клиенты ваших объектов не будут вызывать методы вашего объекта параллельно, то ваши объекты должны быть целиком потокобезопасными. Это означает, что ваши классы должны синхронизовать доступ к нестатическим переменным в дополнение к синхронизации данных разделяемых между экземплярами. Писать потокобезопасный код нелегко, но вы должны быть готовы к этому, если собираетесь использовать MTA.

Правило 3: Избегайте использования TLS в объектах помеченных как ThreadingModel=Free или ThreadingModel=Both

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

Почему? Потому что вызов, попадающий в MTA перемещается в RPC потоки. Каждый вызов запросто может попасть в различные RPC потоки, даже если все они были из одного потока и одного и того же подразделения. Поток B не имеет доступа к локальной памяти потока A, так что если вызов номер 1 попадает в поток A и объект помещает данные в TLS, то если вызов номер 2 попадает в поток B и объект пытается получить данные записанные в TLS при первом вызове, он их не получит. Никак.

Избегайте использовать TLS для сохранения данных между вызовами MTA-шных объектов. Это будет работать, только если все вызовы придут из одного и того же объекта и из того же самого MTA, в котором находится объект.

И последний совет: если вы так и не поняли, зачем нужно то или иное правило, просто следуйте ему, и мир станет лучше.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 1    Оценка 292        Оценить