Понимание потоковых моделей в COM при программировании на Delphi - Совместимость потоковых моделей клиента и сервера
ОГЛАВЛЕНИЕ
Правило #4:
Если клиентское приложение COM задает потоковую модель, не того типа, т.е. несовместимую с типом потоковой модели сервера COM, сам COM гарантирует, что клиент и сервер будут все равно правильно взаимодействовать друг с другом.
Другими словами, COM будет учитывать мнение клиента о том, как он может управлять потоками, и также COM будет учитывать мнение сервера о том, как он может управлять потоками. COM возьмет на себя ответственность за "бесшовную" совместную работу клиента с сервером без каких-либо дополнительных усилий по разработке со стороны программиста. Но Вы можете спросить, как COM может делать это?
В действительности ответ очень прост: Правила #1 и #2 предполагают, что если Вы однажды определили потоковую модель и тип подразделения (подразделений), с которыми работает Ваше приложение, то Вы тем самым заключили обоюдное соглашение с COM в том, что Вы и COM должным образом все подготовили в соответствии с правилами. Например, если Вы говорите COM, что Вы хотите поддерживать только однопотоковую модель, то Вы говорите "Эй, COM. Я гарантирую тебе, что я могу обрабатывать любые запросы только в одном потоке. Если ты нуждаешься во мне, ты можешь делать все только в одном потоке. Не допускай ко мне множественные потоки, так как я не готов обрабатывать их!" COM принимает эти слова как молитву и будьте уверены без дополнительных вопросов, что все Ваши запросы будут удовлетворены. Это означает, что если есть два приложения, клиент и сервер, каждое из которых сказал COM, что он может и что не может делать, то COM обезопасит Вас от кровавых разборок между ними, обеспечив тем самым, что запросы будут правильно восприниматься с обоих сторон, когда они начнут взаимодействовать друг с другом. Если COM не делал бы этого, то потоковая работа COM была бы сущим кошмаром. Подумайте об этом!
Так, где это мы сейчас? Мы установили очень важные базовые правила поведения потоков в COM. Если Вы успели заметить, все это было чистой теорией. Я умышленно сделал это, так как когда я исследовал и изучал эту тему, мне пришлось пройти через большое количество программного кода, который не имел никакого смысла. Поверьте мне, Вы не хотите этого делать и даже использовать его до тех пор, пока Вы не поймете наконец, что этот код делает. Если Вы делали это, то Вы уже познакомились с потенциальными проблемами в Ваших приложениях, и Вы можете насчитать множество часов, проведенных за отладкой, пока Вы однажды не открыли для себя, что использование потоков в COM чревато трудно находимыми ошибками. В конце концов я понял, что следует усвоить основы прежде, чем написать некий код, который будет что-либо делать с потоками в COM. Теперь я бы хотел, чтобы Вы внимательно перечитали все то, что мы уже узнали, чтобы быть уверенным, что Вы, по крайней мере, поняли, о чем же идет речь. Если Вы чувствуете себя уверенно после изучения первых четырех правил, то Вы должны быть готовы двигаться дальше к следующим частям, где мы сконцентрируемся на специфических деталях, окружающих первые четыре правила. Готовы?
Модель однопотокового подразделения (the Single Threaded Apartment Model - STA) В предварительном обсуждении мы установили, что модель STA позволяет Вам разрабатывать многопотоковые приложения COM, в которых каждый поток должен инициализироваться/содержаться внутри своего собственного подразделения. Мы также поняли, что для того, чтобы поток правильно взаимодействовал с COM, он должен сначала войти в подразделение или инициализировать его. Так как же именно поток входит в STA? COM предоставляет функцию CoInitializeEx API для входа потока в подразделение. Синтаксис CoInitializeEx (определенный в ActiveX.pas) следующий:
function CoInitializeEx (pvReserved : pointer; coInit : longint) : HResult; stdcall;
Параметр pvReserved в данном случае бесполезен и должен быть равен NIL. Параметр coInit определяет тип подразделения, в который поток желает войти. Для модели STA Вы должны передавать в качестве этого параметра константу COINIT_APARTMENTTHREADED. Короче, следует писать следующий вызов:
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
который позволяет потоку Вашего приложения войти в подразделение STA (инициализировать его). Мы также уже знаем, что если поток вошел в подразделение, то он должен выйти из него до своего завершения. Для выхода из подразделения COM предлагает следующий вызов CoUninitialize API, имеющий следующий синтаксис.
procedure CoUninitialize; stdcall;
Если поток производит вызов CoInitializeEx дважды подряд, то в первом случае возвращается код завершения функции (HResult), равный S_OK. Во второй раз (и во все последующие) будет возвращено значение S_FALSE, при этом никаких действий не производится. Хотя некоторые авторы и не рекомендуют производить лишние вызовы CoInitializeEx (Прим. перев. - так у автора; вероятно должно быть CoUninitialize), мои исследования показывают, что Вам все равно следует обращаться к CoUninitialize ДЛЯ КАЖДОГО вызова CoInitializeEx вне зависимости от
того, вернул ли предыдущий вызов S_OK или S_FALSE. Это склоняет меня к мысли, что исполняющая система COM ведет счетчик количества входов в STA одного и того же потока, и позволяет потоку успешно покинуть STA только в том случае, если счетчик входов равен нулю. Например:
begin
// вход в STA
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
// этот второй вызов организует повторный вход в текущий STA
// (возвращает код завершения <S_OK>)
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
// этот вызов закрывает последний CoInitializeEx, но выход из STA не производится
CoUninitialize;
// этот вызов закрывает первый CoInitializeEx и окончательно покидает STA CoUninitialize;
end;
После входа потока в подразделение недопустимо изменять (или входитьпо-другому) подразделения до тех пор, пока поток не покинет это подразделение. Это означает, что если поток вошел в STA, а затем производит вызов CoInitializeEx, осуществляющий вход в MTA (вход в MTA обсуждается позже в данной статье), последний вызов CoInitializeEx не будет успешным и вернет код своего завершения, равный RPC_E_CHANGED_MODE ($80010106), означающий "Нельзя изменять тип потока после того, как он был установлен" ("Cannot change thread mode after it is set"). В этом случае Вы не должны производить вызов соответствующего CoUninitialize. Другими словами вызов CoUninitialize должен производиться только в том случае, если вызов CoInitializeEx вернул S_OK или S_FALSE. Если он вернул что-либо другое Вы не должны вызывать CoUninitialize.
COM предлагает и более ранний вызов API - CoInitialize, который функционально эквивалентен CoInitializeEx (NIL, COINIT_APARTMENTTHREADED). Синтаксис CoInitialize следующий:
function CoInitialize (pvReserved : pointer) : HResult; stdcall;
Таким образом, следующий вызов очень похож на вызов CoInitializeEx, показанный ранее:
CoInitialize (NIL);
Другая пара ранних вызовов API, достойная внимания - это OleInitialize и OleUninitialize. OleInitialize эквивалентен CoInitialize, но он инициализирует также и подсистему пользовательского интерфейса COM (такую как "тащи и бросай - drag-and-drop" OLE). OleUninitialize эквивалентен вызову CoUninitialize, но он дополнительно деинициализирует подсистему пользовательского интерфейса COM, инициализировавшуюся вызовом OleInitialize. Это означает, что если Ваше приложение COM использует, скажем, OLE drag-and-drop вызов CoInitialize непригоден; Вам необходимо использовать вызов OleInitialize.
Одно Вам необходимо запомнить. Вызов CoInitializeEx возможен только в DCOM для Windows NT4 или если установлено расширение DCOM для Windows 95. Если Вы производите вызовы CoInitializeEx на машине, не поддерживающей указанных расширений DCOM, Вы будете получать сообщение об ошибке, что данный вызов не поддерживается, а Ваша программа не будет работать. Следовательно, если Вы интересуетесь только моделью STA, использование пары вызовов CoInitialize/CoUninitialize будет давать хорошие результаты вне зависимости от того, установлены у Вас расширения DCOM или нет. Если Вы удивляетесь, почему Вам никогда не требовалось производить вызовы CoInitialize/CoUninitialize в Ваших однопотоковых приложениях на Delphi, то это потому, что они уже были выполнены за Вас модулем ComObj.pas. Модуль ComObj производит вызов CoInitialize(NIL) во время своей инициализации, а вызов CoUninitialize - во время финализации. Это означает, что главный поток Вашего приложения всегда живет в STA. Важно отметить, что если Вам необходимо создавать другие потоки STA в Вашем приложении, взаимодействующем с COM, то Вы должны делать вызовы CoInitialize/CoInitializeEx/CoUninitialize явно, как это было описано выше. Если Вы забываете сделать это, то получаете ошибочный код завершения CO_E_NOTINITIALIZED ($800401F0), означающий "Не был вызван CoInitialize" ("CoInitialize has not been called") из потока, начинающего взаимодействие с COM. Мы узнали, что STA должен содержать в точности один поток, и, что в случае попытки множества потоков взаимодействовать с объектом COM, живущим в STA, COM будет уверен, что эти потоки могут обслуживаться только одним потоком, живущим в этом STA, в данный момент времени. Проще, COM всегда будет осуществлять все вызовы в потоке STA последовательно. Этот последовательный процесс возможен потому, что (для STA) COM автоматически создает скрытое системное окно, управляющее всеми системными вызовами для каждого ассоциированного STA. Так в действительности, когда нескольким потокам необходимо произвести одновременные обращения к объекту, живущему в STA, COM странслирует эти вызовы в окно сообщений и поставит эти сообщения в очередь один за другим, т.е. в очередь сообщений одного потока, живущего в Вашем STA. Таким образом, сообщения в очереди могут быть проверены и исполнены по одному Вашим потоком STA. Ваш поток будет всего-навсего пропускать (или передавать) эти сообщения скрытому окну для дальнейшей обработки. Это подводит нас к наиболее важному правилу для STA: