Для зарегистрированных пользователей |
|
Понимание потоковых моделей в COM при программировании на Delphi
Потоковые модели в COM имеют репутацию наиболее сложных для понимания. Возможно потому, что множество имеющейся документации по этой теме имеет "техническую природу" или ориентировано на конкретный язык, чаще всего C или C++. Цель этой статьи - дать Вам возможность понять, почему потоковые модели в COM так важны и как правильно использовать потоковые модели в Ваших приложениях COM. Моя цель - представить Вам материал таким образом, чтобы Вы могли читать его последовательно от начала до конца и в результате понять всю статью. Сказав это, я бы настойчиво рекомендовал Вам не пропускать ни одной страницы в процессе чтения, чтобы у Вас не возникло трудностей оттого, что Вы что-то пропустили раньше. Прежде, чем начать изложение, давайте начнем с того, что поймем, почему потоковые модели так важны для Ваших приложений COM. Исходя из своего опыта, я могу сказать, что наиболее существенной причиной использования потоковых моделей является повышение общей производительности и скорости реакции Вашей программы, особенно для объектов серверов COM, которые используются для обслуживания большого количества клиентских приложений. Но я не хочу сказать, что использование потоковых моделей в Ваших объектах серверов COM всегда увеличивает производительность. Вы должны тщательно изучить, как используются Ваши объекты и как потоковая модель повлияет на производительность приложения и целостность данных. Я должен подчеркнуть, что вопросы целостности обязательно должны рассматриваться объектов при принятии решения, применять или нет потоковую модель. Несмотря на то, что Вы можете думать, что использование потоковой модели существенно повысит производительность объекта, может оказаться, что Ваши объекты сильно зависят, скажем, от третьих библиотек, которые могут "не выжить" в условиях многопоточности. Другой хорошей причиной применения многопоточности может быть то, что задача по своей природе является весьма пригодной для многопоточной реализации. Например, серверные объекты, являющиеся чисто служебными объектами, вероятно, могут сильно зависеть от времени при выполнении операций или захватывании ресурсов. Примерами таких объектов являются мониторы работы оборудования, объекты пакетной обработки или даже простые объекты манипулирования данными, время исполнения которых для успешного завершения непредсказуемо. В этих случаях тип разрабатываемого Вами приложения по существу определяет использование многопоточности. Имеется множество других причин, при которых Вы могли бы использовать многопоточность, но две упомянутые выше причины являются наиболее общими среди наблюдаемых в промышленном программировании. С другой стороны, я бы хотел предупредить, что не стоит применять многопоточность, если Вы не нуждаетесь в ней или не можете понять преимущества получаемого при этом решения. Это означает, что Вам не стоит даже думать о многопоточности, если Вы думаете только о том, что это круто. Поверьте мне, использование мнопоточности существенно усложняет Ваше приложение и, если Вы недостаточно все продумали, Вам придется искать ошибки в тех местах программы, которые прекрасно работали в однопоточном исполнении.
Основы многопоточности и COM Многопоточность в COM очень легко понять. На самом деле! Нужно всего лишь потратить время на освоение большого количества новой информации! Одной из "непростительных" причин того, что изучение многопоточности в COM столь трудно, является страх перед хорошо звучащими (но непонятными) словечками, такими как: подразделения (apartments), однопоточные подразделения (STAs), многопоточные подразделения (MTAs), свободное использование потоков (free threading), маршалинг интерфейса (interface marshaling) и т.д. Но это все только громко звучащие слова и ничего более. В действительности же имеется множество хорошо определенных правил, которым очень легко следовать для полного понимания многопоточности в COM. Все, что Вам нужно - это знать, что это за правила, выучить их, а затем действительно использовать их в своих приложениях. Я бы хотел начать разговор о первом и наиболее общем правиле, а затем идти дальше и дальше к специфическим правилам далее в этой статье.
Правило #1: Каждое приложение, использующее COM, должно сообщить COM, как оно будет управлять потоками исполнения (никаких если, никаких но).
Это правило является важным, так как для того, чтобы COM могла бы взаимодействовать с Вашим приложением или Ваше приложение могло бы взаимодействовать с другими приложениями посредством COM, COM должна знать как правильно делать вызовы объектов Вашего приложения на уровне той потоковой модели, которую Вы указали. Это имеет смысл, так как COM является тем клеем, который используется приложениями для взаимодействия друг с другом и обеспечивает корректность взаимодействия многопотоковых приложений. COM, по крайней мере, должен знать, как приложения, которые он связывает вместе, управляют многопоточностью. Этот уровень "потоковой трудности" называется потоковой моделью COM. Для целей этой статьи мы определим 3 потоковых модели. В действительности нет никакой разницы, как их назвать. И так как мы понимаем, что каждая из них представляет из себя, я не буду больше беспокоиться о том, как Вам хотелось бы их называть.
Однопотоковая модель (the single-threaded model): Если приложение работает в однопотоковой модели, то это означает, что в этом приложении имеется только один поток (thread) исполнения, в котором приложение взаимодействует с COM.
Это подразумевает, что COM будет гарантировать взаимодействие с Вашим приложением (производить обращения к нему) таким образом, что не будет одновременных обращений в разных потоках. Когда COM хочет чего-либо от Вашего приложения, он будет производить это только в главном потоке Вашего приложения. Это также подразумевает, что если у Вас есть однопотоковый сервер COM, который сейчас имеет, скажем, 50 объектов, используемых (разделяемых) 50-ю клиентами и, если все 50 клиентов пытаются вызвать метод каждого объекта в одно и то же время, то COM вмешается и не позволит выполниться 50 потокам на Вашем сервере одновременно. Вместо этого COM сделает так, что вызовы методов будут произведены в одном потоке один после другого, пока все 50 объектов не удовлетворят вызовы 50 методов (и, конечно же, каждый метод будет возвращать результат работы непосредственно после своего завершения). Как Вы видите в этом примере приведен крайне неэффективный сервер для такого режима его использования. Представьте себе, что каждый метод требует 1 секунду времени для своего завершения. Потребуется, по крайней мере, 50 секунд для обслуживания 50 одновременных обращений и, что еще хуже, пока не будет обслужен первый вызов, никакие другие вызовы обслуживаться не будут. Верите или нет, но Delphi 3 технически может создавать только однопотоковые внутренние и внешние сервера COM. Хотя это не может выглядеть ограничением для внутренних серверов (DLL), но, поверьте мне, это очень плохо работает в случае с внешними серверами (EXE), когда множество клиентов должны обслуживаться одновременно с разумным временем отклика.
Модель однопотокового подразделения (the single-threaded apartment model - STA):Не вдаваясь в подробности сейчас (эта модель будет полностью объяснена позже), эта модель позволяет COM взаимодействовать с Вашим приложением в нескольких подразделениях (apartment), каждое из которых содержит в точности один поток (thread).
Вы можете подумать: Ух ты! Что за чертовщина это подразделение? Простейшее определение, которое я видел, звучит так: подразделение - это хорошо определенная инкапсуляция того, как потоки и объекты COM взаимодействуют друг с другом. Когда я говорю "инкапсуляция", я подразумеваю, что некоторый поток и некоторый объект, которые должны что-то делать совместно с COM определены в понятии "подразделение", в котором они "живут". Другими словами, Вы не можете описать как поток и объект COM взаимодействуют друг с другом без определения сначала понятия "подразделение", которое содержит как поток, так и объект. Когда я говорю "хорошо определенная", я подразумеваю, что взаимодействие между данным потоком и данным объектом COM связано набором правил, определяемых типом подразделения, в котором этот поток и этот объект размещены. Мы перейдем к изучению этих правил позже в этой статье, но сейчас давайте просто скажем, что эти правила имеют хорошо определенные спецификации в COM. Давайте сделаем это более понятным путем определения другого правила:
Правило #2: Каждый поток в Вашем приложении, использующий COM, сначала должен войти в подразделение или инициализировать его (никаких если, никаких но) . Если поток вошел в подразделение или инициализировал его, он должен покинуть его до того как завершится сам (никаких если, никаких но).
Просто полагайте, что подразделение - это то место, где "живет" поток. Причиной того, почему поток, использующий COM, должен войти в подразделение, является тот факт, что COM должен внутренне инициализировать себя таким образом, чтобы знать, как правильно взаимодействовать с потоком. Причиной того, что поток должен покинуть подразделение до своего завершения, является тот факт, что это единственный способ уведомить COM о том, что поток больше не желает использовать это подразделение и что COM теперь может делать все, что ему нужно, чтобы освободить ресурсы, использовавшиеся подразделением. Если поток входит в подразделение и затем создает объект COM, то этот объект описывается как "расположенный" или "живущий" в этом подразделении (в действительности это не совсем аккуратное определение, но для целей простоты и легкости понимания подразделений давайте сейчас предполагать, что это так). Теперь, если различные потоки (каждый из которых может быть, а может и не быть потоком, первоначально создавшим объект) пытаются манипулировать этим объектом, COM может вмешиваться в их работу с полной уверенностью, что доступ к этому объекту производится именно тем способом, который определяется типом подразделения, в котором он живет. В чем же заключаются правила однопотокового подразделения? Очень просто!
Во-первых, однопотоковое подразделение (STA) - это подразделение, содержащее только один поток, взаимодействующий с COM. Во-вторых, если различные потоки пытаются взаимодействовать с STA или, точнее, с объектом COM, живущим в STA, COM будет гарантировать, что все потоки будут обслуживаться по принципу "только один одновременно", единственным потоком, который первым вошел или, что существенно, создал это подразделение. В-третьих, если Ваше приложение желает использовать модель однопотокового подразделения и потребуется взаимодействие нескольких потоков с COM, то Ваше приложение будет вынуждено создать несколько однопотоковых подразделений, каждое из которых соответствует единственному потоку. Чтобы легче было запомнить эти правила, давайте я приведу пример.
Пусть Вы создали объект COM X в потоке STA. Пусть X имеет один метод с именем Method1. Теперь, если два или более потоков (очевидно, что каждый из них живет в своем STA) пытаются одновременно вызвать X.Method1 в одном экземпляре X, COM в соответствии с моделью STA, будет гарантировать, что X в действительности не бомбардируется одновременными вызовами Method1 в разных потоках. Вместо этого COM обеспечит, что X.Method1 будет вызван первым запросившим потоком, затем следующим, затем еще следующим и т.д. пока ВСЕ вызовы не будут обслужены одним потоком, который первоначально создал подразделение, в котором живет X. Другими словами, вызовы методов объекта COM, живущего в STA, гарантировано будет следовать последовательно один после другого вне зависимости от того, откуда пришел этот поток.
Теперь, если Вы создадите два экземпляра объекта X, каждый в отдельном STA и вызовите метод X.Method1 в обоих экземплярах одновременно, COM позволит обоим вызовам исполняться одновременно, так как каждый раздельный STA имеет свой собственный поток. Поэтому основная разница между моделью STA и однопотоковой моделью заключается в том, что при однопотоковой модели может быть только один поток на все приложение, в то время как в модели STA Вы можете иметь несколько потоков, каждый из которых исполняется в собственном STA. Если Вы немного подумаете, то придете к выводу, что однопотоковая модель - это все лишь дегенеративный вариант модели STA, в которой имеется только один STA на все приложение.
Модель многопотокового подразделения (the multithreaded apartment model - MTA): Модель многопотокового подразделения (MTA) просто является расширением модели STA. Если Вы еще способны следить за мной, легко сделать вывод, что MTA является подразделением, в котором могут размещаться несколько потоков, в то время как в STA в подразделении может размещаться только один поток. Основная разница между моделями STA и MTA заключается в том, что если Вы создаете объект в MTA и используете его из нескольких потоков, COM осуществляет одновременный доступ к нему в отличие от модели STA, при которой осуществляется последовательный доступ к объекту. Как я уже говорил ранее, тип подразделения определяет, каким образом потоки взаимодействуют с объектами COM, живущими в этом подразделении. Если Вы создаете объект в MTA, COM будет гарантировать, что поступающие вызовы методов могут происходить одновременно из нескольких потоков. Как Вы можете видеть, программирование объектов, которые Вы собираетесь использовать в модели MTA, должно осуществляться таким образом, чтобы они могли обрабатывать поступающие запросы от любого потока в любое время и, в то же время, сохранять свою целостность. Другое правило, которое Вы должны всегда помнить, заключается в том, что один процесс, взаимодействующий с COM, может содержать только один MTA. Это означает, что если Вам необходимо создать несколько потоков, взаимодействующих с COM в MTA, то каждый из этих потоков должен входить в единственный MTA Вашего процесса. Мы только что определили 3 различных типа потоковых моделей, доступных в COM. Однопотоковая модель может быть описана как самая слабая из всех, когда дело касается использования потоков в приложении, т.е. она не позволяет обрабатывать одновременные запросы COM из разных потоков. Модель однопотокового подразделения (модель STA) немного сложнее, чем однопотоковая модель. Это выражается в том, что она позволяет взаимодействовать COM с несколькими потоками, но может сделать это только путем создания нескольких подразделений. Это подразумевает, что STA не является существенно более сложной, чем однопотоковая модель. При этом каждый STA имеет только один поток, который и будет обрабатывать все взаимодействие с COM в пределах подразделения.
MTA, с другой стороны, является наиболее сложным из всех перечисленных в смысле взаимодействия с COM нескольких потоков. Другими словами, объект, живущий в MTA, говорит внешнему миру "Я сложно устроен! Я могу работать с потоками где угодно и в любое время!". Перед тем, как продолжить дальше, я бы хотел отметить некоторые термины. Модель STA часто называется проще как "работающие в одном потоке" (apartment-threaded), а модель MTA как "работающие во многих потоках" (free-threaded). Хотя мне и не хочется использовать эти термины в своей статье, но чувствуйте себя свободно и подменяйте названия моделей STA и MTA на них, если они понятнее для Вас. Это только слова и ничего более!
Правило #3: Объекты COM создаются в потоках, потоки живут в подразделениях, а подразделения живут в процессах.
Первое интуитивно понятно: объект COM может существовать только в том случае, если он создан, объект COM может быть создан только в том случае, если поток желает создать его. Второе легко понять, применив Правило #2, т.е., если каждый поток, который нуждается во взаимодействии с COM, должен войти в подразделение или инициализировать его, то, следовательно, это подразделение, так сказать, является вместилищем потока. Третье, подразделения живут в процессах, может не очень легко "визуализовать" до тех пор, пока Вы не начнете писать код, взаимодействующий с подразделениями. Не беспокойтесь, что Вы пока не согласны со мной в том, что подразделения живут в процессах, это придет позже, когда у Вас появится некоторый опыт, а сейчас просто запомните это правило, так как оно понадобится для понимания нижеследующих концепций.
Правило #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
// (возвращает код завершения )
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:
Правило #5: Для того, чтобы сервер STA работал бы правильно, его поток должен включать в себя цикл, непрерывно проверяющий наличие оконных сообщений (windows messages) и, соответственно, их обрабатывающий.
Другими словами, следующий фрагмент показывает основы реализации работающего потока STA:
var rMsg : TMsg;
begin
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);у
{ главный цикл сообщений потока }
while (GetMessage (rMsg, 0, 0, 0)) do
DispatchMessage (rMsg);
CoUninitialize;
end;
Если Вы планируете создать поток STA, не имеющий цикла обработки оконных сообщений, Вы должны учитывать, что если Вы пытаетесь получить доступ к объекту, созданному в этом STA, из других потоков (очевидно, в других STA), Ваши вызовы не будут успешно выполнены и Ваш вызывающий поток "зависнет". Очевидно, что причина происходящего кроется в том, что мы только что узнали. Я бы хотел подчеркнуть также, что цикл сообщений необходим только с той точки зрения, что Ваш поток STA собирается обслуживать объекты для потоков в других подразделениях. Действительная цель цикла обработки оконных сообщений заключается в том, что COM может выстраивать последовательно все вызовы, приходящие от всех потоков. Другими словами, если Вы собираетесь создать поток STA, который работает с объектами COM внутри того же самого потока, то нет абсолютно никакой нужды в цикле сообщений.
Например, если клиентское приложение создает поток STA и внутри этого потока создает объект COM, работает с ним и затем завершает, то нет никакого смысла в цикле сообщений вообще, т.е.
procedure TMySTAThread.Execute;var
pObject1 : IObject1;
begin
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
// Создает объект object1, делает с ним что-то (something) и завершается
pObject1 := CreateOleObject ('Server.Object1') as IObject1;
pObject1.DoSomething;
pObject1.DoSomeOtherThing;
pObject1 := NIL;
// Цикл сообщений не нужен, так как мы уже все сделали
CoUninitialize;
end;
Очевидно, что если Вы планируете использовать модель STA в Вашем приложении, то по существу говоря это приложение является многопоточным, в котором каждый поток живет в отдельном STA. Это означает, что для COM имеется возможность делать одновременные вызовы Вашего приложения, если оно создает несколько STA (отметьте существенную разницу: для каждого STA вызовы выстраиваются последовательно, но в целом приложение может иметь несколько STA, вызовы к которым могут поступать одновременно). Это приводит нас к другому правилу, весьма важному для STA:
Правило #6: Благодаря модели STA Ваши объекты STA могут защищать некоторые глобальные (в масштабах приложения) данные, которые могут быть разрушены при одновременном доступе множества объектов из раздельных потоков. Любые данные, характерные для данного экземпляра (обычно это поля, содержащиеся в объявлении класса Вашего объекта), уже являются потоково-безопасными и не нуждаются в защите.
Для иллюстрации сказанного рассмотрим следующее: 2
var
iGlobal : integer;
type
TObject1 = class (TAutoObject)
protected
FLocal : integer; procedure AccessGlobal; procedure AccessLocal; end;
procedure TObject1.AccessGlobal; begin // Если создается несколько экземпляров TObject1 в раздельных STA и // AccessGlobal вызывается в каждом из них одновременно, то возможно, // что iGlobal будет испорчена
// делаем что-либо с глобальной переменной iGlobal := iGlobal + 1; end;
procedure TObject1.AccessLocal; begin // Если создается один экземпляр TObject1 в STA и множество потоков // пытаются вызвать AccessLocal в этом единственном экземпляре, // то локальная переменная гарантировано не будет испорчена, так как // COM обеспечит, что вызовы AccessLocal следуют последовательно // друг за другом в пределах одного потока, живущего в этом STA
// делаем что-либо с локальной переменной FLocal := FLocal + 1; end; Следовательно, правильным способом исправить TObject1.AccessGlobal является сделать переменную iGlobal защищенной от одновременного доступа из нескольких потоков STA. Простейший способ сделать это - это использовать критические секции Win32, которые обслуживаются операционной системой для таких случаев (я буду полагать, что Вы уже знаете некоторые основы потоков и функций работы с ними, существующие в Win32. Вы должны обратиться к книгам, если Вы чувствуете пробел в своих познаниях в этом месте. Вот книга, которую я нашел достаточно полезнойТогда TObject1.AccessGlobal может быть переписана следующим образом:
uses SyncObjs;
var csGlobal : TCriticalSection; iGlobal : integer;
procedure TObject1.AccessGlobal; begin // вход и выход из критической секции теперь гарантирует, что доступ // к iGlobal может быть осуществлен одним потоком единовременно и // невозможно разрушение при доступе из различных потоков csGlobal.Enter; try // делаем что-либо с глобальной переменной iGlobal := iGlobal + 1; finally csGlobal.Leave; end; { finally } end;
initialization csGLobal := TCriticalSection.Create; finalization csGlobal.Free; end.
Стороннее замечание. Delphi предоставляет в Ваше распоряжение класс TCriticalSection, являющийся оболочкой для примитивов критической секции Win32. TCriticalSection располагается в модуле SyncObjs, который, как я знаю, существует только в версии клиент/сервер Delphi. Если Вы не располагаете приобретенной копией клиент-серверной версии Delphi (как я), не все потеряно: Вы можете использовать класс TCriticalSection из библиотеки ThreadComLib этой статьи или, если Вы чувствуете в этом силу, использовать вызовы Win32 API.
Подведем небольшой итог. Я как всегда отмечаю, что к объекту COM, живущему в STA можно получить доступ из множества потоков, живущих в других STA и что в случае одновременного доступа к нему все потоки выстраиваются в очередь вне зависимости от того, откуда этот поток пришел. Я Вам еще не сказал, что это гарантируется самим COM, но для того, чтобы COM имел возможность правильно исполнить эту возможность, Вы со своей стороны должны проделать некоторую работу. Для ясности в будущем, давайте предположим, что Вы имеете два потока STA в Вашем клиентском приложении и Вы создаете объект COM в первом потоке. Если Вы хотите, чтобы второй поток имел возможность доступа к этому же объекту (созданному в первом потоке), то Вы не можете просто получить указатель на интерфейс к объекту (созданному из первого потока) и начать вызывать его методы. Что Вы должны делать или, что более важно, COM позволяет делать Вам - это получить указатель на интерфейс и каким-то образом транслировать его из потока 1 в поток 2 так, чтобы когда поток 2 попытается получить доступ к этому интерфейсу COM знал бы, что этот интерфейс действительно используется вторым (отличным от первого) потоком и, таким образом, чтобы он знал о необходимости установления очередности с первым STA. Чтобы не забыть, сформулируем это в виде правила:
Правило #7: Для обеспечения корректного доступа к объектам COM множества потоков в РАЗДЕЛЬНЫХ подразделениях, указатель на интерфейс к этому объекту должен маршалироваться (транслироваться) из подразделения, в котором этот объект живет, в подразделение, в котором должен производиться доступ .
Конечно, если доступ к объекту производится из своего собственного подразделения, то нет нужды ни в каком маршалинге, хотя это и не повредит. Причина этого заключается в том, как мы уже говорили ранее, что подразделение имеет хорошо определенный набор правил, как следует предоставлять доступ к объектам, живущим в нем. Для того, чтобы доступ к объекту потоками из других подразделений осуществлялся должным образом, Вы должны, когда понадобится доступ к объекту из другого подразделения, явно объявить COM, что правила доступа к этому объекту установлены корректно. Если Вы забываете производить маршалинг через подразделения при доступе к объектам COM и пытаетесь манипулировать простым указателем на интерфейс, COM выдаст Вам ошибку с кодом RPC_E_WRONG_THREAD ($8001010E), означающую "Приложение вызвало интерфейс, маршалированный для другого потока" ("The application called an interface that was marshaled for a different thread").
Когда Вы маршалируете указатель на интерфейс из подразделения источника в целевое подразделение, Вы экспортируете указатель из подразделения источника и затем импортируете тот же указатель в целевое подразделение. Такой процесс экспорта-импорта приводит к тому, что целевое подразделение получает нечто, называемое заместителем или прокси (proxy) интерфейсным указателем на оригинальный указатель на интерфейс. Для всех целей прокси ведет себя точно так же, как и оригинальный указатель на интерфейс, т.е. Вы можете вызывать методы через прокси так же, как это производится и через настоящий указатель на интерфейс. Единственная разница, которую Вам потребуется знать, заключается в том, что вызовы через прокси делаются медленнее (иногда существенно медленнее), чем вызовы, производимые через настоящий указатель не интерфейс. Экспорт и импорт указателя на интерфейс в действительности не столь сложен. COM предлагает две простых функции API для маршалинга указателей на интерфейс: CoMarshalInterThreadInterfaceInStream - для экспорта/маршалинга и CoGetInterfaceAndReleaseStream - для импорта/демаршалинга. Демаршалинг Вам необходимо производить потому, что процесс маршалинга записывает указатель на интерфейс в поток (stream) (вместе с другой информацией, однозначно иденцифицирующей размещение объекта COM, на который ссылается указатель), а, следовательно, Вам придется изымать его оттуда (демаршалировать) в допустимый прокси указатель на интерфейс. Синтаксис CoMarshalInterThreadInterfaceInStream следующий:
function CoMarshalInterThreadInterfaceInStream (const iid: TIID;
unk: IUnknown; out stm: IStream): HResult; stdcall;
IID идентификатор интерфейса (ID), который Вы хотите маршалировать. Unk - это указатель на интерфейс IUnknown объекта, чей интерфейс Вы хотите маршалировать.
Stm - это переменная IStream, в котрой будет содержаться указатель на маршалируемый интерфейс, Вы не должны запрашивать какую-либо память для Stm, так как COM сделает это за Вас.
Синтаксис CoGetInterfaceAndReleaseStream следующий:
function CoGetInterfaceAndReleaseStream (stm: IStream; const iid: TIID; out pv): HResult; stdcall;
Stm - это переменная IStream, которая используется Вами при маршалинге указателя на интерфейс; COM автоматически освобождает память Stm после завершения вызова. IID идентификатор интерфейса (ID), который Вы хотите маршалировать. Pv - переменная, в которой хранится прокси указатель интерфейса до успешного завершения демаршалинга.
Следующий фрагмент иллюстрирует, как маршалируется указатель на IDispatch из потока 1 в поток 2 различных STA:
var pStream : pointer;
В потоке 1:
procedure DoMarshal; var pObject1 : IDispatch; begin pObject1 := CreateOleObject ('Server.Object1'); CoMarshalInterThreadInterfaceInStream (IDispatch, pObject1 as IUnknown, IStream (pStream)); end;
В потоке 2: procedure DoUnmarshal; var pObject1 : IDispatch; begin CoGetInterfaceAndReleaseStream (IStream (pStream), IDispatch, pObject1); pObject1.DoSomething; end;Отметьте, что в маршалинге важна последовательность производимых действий: сначала указатель на интерфейс должен быть маршалирован в потоковой переменной, и только после этого второй поток может демаршалировать его. Я также объявил pStream как указатель вместо IStream и делаю явное преобразование типов IStream (pStream), так как мы хотим предотвратить автоматический подсчет ссылок в Delphi, из-за которого у нас могли бы возникнуть проблемы - функции маршалинга Win32 API неявно создают и освобождают указатель на IStream (pStream). Теперь, когда мы приколотили основные идеи модели STA, мы готовы двигаться дальше к следующей ступени понимания: как клиенты COM и сервера взаимодействуют между собой во владениях модели STA. Этот шаг приводит к теме, возможно, являющейся одной из самых важных (и наиболее неподдающейся пониманию) тем в потоковой модели COM. Поэтому я буду предполагать, что Вы замедлили чтение на этом месте. Постарайтесь восстановить в памяти полную картину прочитанного и, если необходимо, перечитайте еще раз.
Внешний (EXE) сервер, поддерживающий модель STA, обычно создает несколько потоков STA для своих объектов. Простейшей реализацией сервера STA является создание раздельных потоков STA для каждого экземпляра каждого объекта, создающихся средствами этого сервера. Другими словами, если клиент 1 создает Server.Object1, а клиент 2 создает Server.Object2, оба с помощью одного сервера STA, то сервер может просто создать два потока STA: первый, в котором будет жить объект Object1, и второй, в котором будет жить Object2. Таким образом, если клиент 1 и клиент 2 одновременно вызывают методы объектов Object1 и Object2 соответственно, то сервер в действительности будет обслуживать одновременно оба вызова к обоим объектам в двух потоках STA. Простейшим способом для сервера создать по потоку STA на каждый экземпляр является запуск некоего процесса, в котором COM попросил бы сервер создать новый экземпляр объекта. Запустив такой процесс, сервер может затем породить новый поток STA, имея такой поток - создать экземпляр во время исполнения его метода Execute, а затем вернуть указатель на этот экземпляр COM (а, следовательно, и клиенту). Для объектов автоматизации Delphi этот процесс может быть воспроизведен в методе TAutoObjectFactory.CreateInstance, использующемся при реализации IClassFactory.CreateInstance. Следующий псевдокод иллюстрирует этот процесс:
type TSTAAutoObjectFactory = class (TAutoObjectFactory, IClassFactory) function CreateInstance; end;
TSTAThread = class (TThread) procedure Execute; override; end;
function TSTAAutoObjectFactory.CreateInstance; begin Создает и порождает новый экземпляр TSTAThread; Заставляет поток STA создать затребованный объект Ждет, пока поток STA успешно создаст объект Возвращает созданный экземпляр в качестве результата этого метода end;
procedure TSTAThread.Execute; begin // Вход в STA CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
Создает экземпляр, затребованный TSTAAutoObjectFactory; Сигнализирует потоку TSTAAutoObjectFactory.CreateInstance, что экземпляр теперь доступен Вход в цикл сообщений STA
// Выход из STA CoUninitialize; end;
Этот процесс теоретически очень прост, но что Вы могли еще не разглядеть, так это то, что каждый поток STA должен знать, как и когда необходимо прекращать существование всех уже несуществующих объектов COM, живущие в нем, т.е. уже не имеют ссылок к ним от клиентов. Причина этого очевидна: потоки потребляют системные ресурсы и, следовательно, должны быть приняты все меры для того, чтобы каждый поток в приложении завершался бы должным образом, если в нем больше нет нужды. Под "завершался должным образом" я подразумеваю, что Вы должны использовать вызов TerminateThread Win32 API для завершения потоков и выполнить все проверки, убеждаясь, что метод Execute Вашего наследника TThread полностью завершился, так как в противном случае часть использовавшихся ресурсов окажется не освобожденной. Одним из способов решения этой проблемы является организация счетчика всех объектов на поток STA, на самом деле на подразделение. Кто-то также должен отслеживать состояние этого счетчика при создании и ликвидации объектов в этом подразделении. При этом, как только счетчик станет равным нулю, мы можем безопасно прекращать все потоки, живущие в данном подразделении. Вот псевдокод для этого процесса:
type TSTAAutoObject = class (TAutoObject) procedure Initialize; override; destructor Destroy; override; end;
procedure TSTAAutoObject.Initialize; begin inherited; Увеличение счетчика объектов для STA, в котором живет экземпляр; end;
destructor TSTAAutoObject.Destroy; begin inherited; Уменьшение счетчика объектов для STA, в котором живет экземпляр; Если счетчик объектов STA = 0 то сигнализировать потоку STA о необходимости завершения; end;
Для серверов STA создание нового потока STA для экземпляра не всегда может быть лучшим выбором. Как я уже говорил ранее потоки потребляют некоторые системные ресурсы, и если, например, клиенты могут создавать одновременно тысячи экземпляров объектов (а, стало быть, и тысячи потоков STA), то, соответственно, производительность компьютера, на котором работает Ваш сервер, существенно "провалится". Одним из способов гарантировать, что такое не произойдет с сервером STA, является определение каким-то образом максимального числа потоков STA, а затем, когда этот максимум превышен, начать повторное использование существующих потоков STA для обслуживания потребностей новых объектов. Такая технология называется "пул потоков STA" (STA thread pooling). При этом устанавливается предопределенное максимальное количество потоков STA и, когда количество созданных потоков STA превышает это число, новые экземпляры создаются путем использования уже существующих потоков STA из пула. Этот процесс иллюстрируется следующим псевдокодом:
type TSTAAutoObjectFactory = class (TAutoObjectFactory, IClassFactory) function CreateInstance; end;
function TSTAAutoObjectFactory.CreateInstance; begin if (Количество потоков STA в сервере < макс. допустимого числа потоков STA) then Создать и породить новый экземпляр TSTAThread else Выбрать произвольный поток STA из пула существующих;
Здесь поток STA создает затребованный объект Ждет, пока поток STA успешно создаст объект Возвращает созданный экземпляр в качестве результата этого метода end;
Для клиента STA, взаимодействующего с внешним сервером STA, все намного проще. Все, что нужно делать клиенту, это создавать объекты в любых, созданных клиентом потоках STA, а сервер STA будет заботиться о деталях того, как создавать или размещать потоки STA, в которых в действительности живут объекты. Необходимо заметить важную деталь: когда клиент создает объект из сервера STA, в действительности имеются два подразделения, один на сервере, где собственно находится объект, и второй - в клиенте, где размещаются другие клиентские STA, "ощущаемые" как объекты. В действительности клиентский STA содержит прокси, маршалирующий кросс-процесс от сервера к клиентскому приложению. Если клиентский поток STA создает объект из внешнего сервера STA и передает указатель на этот объект (очевидно в режиме маршалинга) второму клиентскому потоку STA, COM будет уверен, что второй STA будет использовать объект по правилам подразделения внешнего сервера, откуда обычно создается этот объект. Если Вы сейчас находитесь в замешательстве, то имеется простой технический прием, который очень просто расскажет Вам, в каком подразделении живет этот объект. Посмотрите на объект, живущий в подразделении с той точки зрения, откуда пришел оригинальный указатель на интерфейс. Так, если в своем клиентском приложении Вы создаете объект в потоке 1 STA и затем используете этот объект из потока 2 STA, то говорят, что объект размещается в STA, в котором живет поток 1, так как оригинальный указатель на интерфейс к этому объекту получаем из этого подразделения. Но, если этот серверный объект размещается во внешнем сервере, то говорят, что объект размещается в STA внешнего сервера, где он располагался первоначально. Всегда помните это технический прием с указателем на интерфейс, так как Вам придется пользоваться им каждый раз, когда у Вас не будет уверенности, в каком из подразделений живет объект.
Мы до сих пор говорили в основном о внешних серверах, давайте теперь поговорим о внутренних серверах (DLL). Обычно STA внутренних серверов не создают явно потоков STA для своих объектов. Причина этого в том, что DLL фактически является просто пассивной библиотекой кода (и данных), отображенной в адресное пространство Вашего клиентского приложения. Поэтому внутренний сервер в действительности эквивалентен любому другому модулю Вашего приложения, однажды отображенным в адресное пространство приложения и, следовательно, все STA, создаваемые клиентом есть то же самое, что и STA, видимые из внутреннего (DLL) сервера. Другими словами, внутреннему серверу вовсе нет нужды активно создавать подразделения и потоки для того, чтобы обслуживать клиента. Что, однако, необходимо, так это (см. Правило #1) объяснить COM, какую потоковую модель он поддерживает или какие типы подразделений его объекты могут обслуживать. STA внутреннего сервера сообщает COM свою потоковую модель путем использования входов системного реестра (registry) для CLSID, регистрируемых для их компонентных классов CoClasses. Точнее, объект во внутреннем сервере, способный размещаться в STA должен добавить строковый параметр "ThreadingModel=Apartment" к параметру HKCR\CLSID\\InprocServer32. Таким образом внутренний сервер, содержащий Object1, поддерживающий STA, должен иметь следующие входы реестра: Что же делает строка "ThreadingModel=Apartment"? Все просто! Указывая ThreadingModel как Apartment сообщаем COM, что когда COM (или клиент) создает Object1, COM может и будет безопасно создавать Object1 непосредственно в том STA, в которое поступил запрос клиента на создание. Это означает, что если клиент создает два потока STA, каждый из которых использует Object1, COM будет счастливо создавать каждый экземпляр так, чтобы он жил в клиентском STA, затребовавшем его создание. Отметьте различия с внешним сервером: внешний сервер явно создает потоки STA, в которых будут жить объекты, в то время как внутренний сервер полагается на COM при определении, в каком STA (или в каком подразделении) необходимо создать объект. Поэтому в действительности внутреннему серверу необходимо иметь строковый параметр ThreadingModel в системном реестре, так как при этой схеме отсутствуют структуры, ответственные за создание потоков STA для их объектов. Следовательно, они должны сказать кому-то, кто отвечает за это (в нашем случае COM), как и где эти объекты могут безопасно жить. Для иллюстрации значимости параметра ThreadingModel, давайте взглянем, что делает Delphi для Ваших внутренних серверов.
Если Вы попробуете посмотреть параметр InprocServer32 для Вашего внутреннего сервера, сделанного на Delphi, Вы увидите, что параметр ThreadingModel отсутствует (у Delphi 3ThreadingModel отсутствует в COM VCL, в Delphi 4 эта возможность включена).
Что это значит? Ну, одно, что можно сказать уверенно, это то, что он не поддерживает потоковую модель STA, однако можно указать "ThreadingModel=Apartment". Так что же случится, если Ваше клиентское приложение создает два потока STA, каждый из которых создает экземпляр, скажем, объекта Object1, который не имеет определенного параметра ThreadingModel? Опять все просто! Отсутствие строкового параметра ThreadingModel означает, что Ваш внутренний сервер поддерживает только однопотоковый режим работы для своих объектов. Другими словами, если имеются два клиентских потока STA, каждый из которых пытается создать экземпляр объекта Object1, COM будет видеть, что Object1 может поддерживать только однопотоковый режим работы и не будет создавать каждый экземпляр непосредственно в каждом клиентском потоке. Что COM будет делать, так это создавать оба экземпляра в одном STA (один STA является техническим определением и реализацией однопотокового режима, в котором имеется только одно подразделение и только один поток в этом одном подразделении) и затем маршалировать (вспомните маршалинг...) указатель на каждый экземпляр из этого STA к STA клиентов, затребовавшим создание этого объекта. Теперь возникает вопрос: COM создает новый STA для содержания обих (и всех прочих) экземпляров или COM только использует один из существующих STA в Вашем клиенте и этот STA используется везде, где необходимо создавать объекты? В этом случае, так как клиент работает на основе STA, COM будет использовать один из STA Вашего клиента, причем это будет главный STA, для доступа ко всем объектам внутреннего сервера. STA, который "зацепит" COM для этой цели - это первый STA, который создает клиентское приложение. Первый STA, создаваемый Вашим клиентским приложением называется главным STA. И последнее, что я бы хотел отметить в связи со строковым параметром ThreadingModel. Строковый параметр ThreadingModel - это только способ, с помощью которого внутренний сервер говорит COM, что он может безопасно поддерживать конкретную потоковую модель. Это означает, что ни при каких обстоятельствах нельзя играть (или даже думать об этом), изменяя параметр ThreadingModel на какое-либо другое значение, до тех пор, пока Вы не будете совершенно уверены, что Ваши объекты могут безопасно обслуживать тот режим, который Вы указали. Поверьте мне, что если Вы поступаете так и думаете, что Ваш внутренний сервер продолжает прекрасно работать, то Вы будете сильно огорчены, услышав, что у Ваших клиентов Ваше приложение начало сбоить, выдавая случайные сообщения о нарушениях защиты памяти, а у Вас не будет никаких мыслей по этому поводу. Другими словами, если Delphi (Delphi 3) не помещает параметр ThreadingModel в системный реестр, то на это имеются причины, и Вы должны сначала определить, что это за причины, прежде, чем Вы начнете изменять параметр ThreadingModel на "Apartment." Из последнего абзаца Вы могли заметить, что каждый клиентский поток STA получает заместителя (proxy), если сервер является однопотоковым. Когда поток подразделения получает заместителя указателя на интерфейс к объекту, это означает две вещи: первое, любой доступ, производимый к этому объекту, замедляется с использованием заместителей (proxy) в сравнении с прямым доступом и, второе, если этот заместитель предназначен для объекта, живущего в STA, то есть большие шансы того, что производительность этого объекта будет очень плохой, если в том же самом STA живут еще несколько объектов. Причина этого в том, что вызовы в этом единственном STA выстраиваются последовательно, и если, скажем, Вы имеете 50 клиентских потоков STA, и они одновременно обращаются к объектам, живущим в одном серверном STA (в случае DLL, 49 клиентов в действительности пользуются заместителями и только один имеет прямой указатель, потому что он является главным STA клиента), COM будет вмешиваться и обслуживать этим единственным потоком STA все вызовы всех потоков один за одним последовательно. Именно в этом кроется причина, почему модель STA, разрешающая Вам создавать много потоков STA, была введена в качестве средства повысить производительность однопотоковых приложений COM.
Теперь, когда мы твердо усвоили, что такое STA, давайте перейдем к следующей части, в которой мы узнаем, как модель MTA продвигает нас дальше к новым возможностям потоков для увеличения производительности наших объектов COM. Модель многопотокового подразделения (the Multithreaded Apartment Model - MTA) Если Вы полностью разобрались в модели STA, то можно быть уверенным, что изучение модели MTA будет просто кусочком торта. Мы говорили ранее, что модель MTA, подобно модели STA, также позволяет разрабатывать многопотоковые приложения COM. Единственно, в чем заключается основная разница между моделью MTA и другими моделями, это то, что модель MTA разработана для получения максимальной производительности Ваших приложений COM. Я должен предупредить Вас, что модель MTA предоставляет Вам максимальную производительность в обмен на большую ответственность со стороны программиста в том, что объекты в действительности могут работать в архитектуре MTA. Следовательно, как и в случае с STA, я не буду, повторяю, не буду рекомендовать разрабатывать приложения COM, поддерживающие модель MTA, до тех пор, пока у Вас не появится совершенно точное понимание того, как работает модель MTA. В соответствии с этим, я имею в виду, что если Вы думаете, что это крутая штука и просто переключаете Ваш работающий сервер STA в режим MTA (простой заменой вызовов CoInitializeEx или изменением строкового параметра ThreadingModel) в надежде, что это будет лучше, то лучше приготовьтесь потерять какое-то время на основательную повторную отладку своего сервера.
Первое, что Вам необходимо знать, это то, как поток входит в MTA. Поток, запрашивающий вход в MTA или его инициализацию, использует тот же вызов CoInitializeEx API, но теперь нам необходимо использовать константу COINIT_MULTITHREADED вместо константы COINIT_APARTMENTTHREADED, использовавшейся для STA. Следующий вызов позволяет потоку войти в MTA: CoInitializeEx (NIL, COINIT_MULTITHREADED); Из Правила #2 мы уже знаем, что если нам необходимо создать несколько потоков, использующих MTA, то каждый из этих потоков должен вызвать CoInitializeEx (NIL, COINIT_MULTITHREADED). Архитектура MTA спроектирована таким образом, что может быть только один MTA на процесс, и, если несколько потоков входят в MTA, все они живут в этом самом единственном MTA. Почему так?
Правило #8: Модель MTA позволяет достичь максимальной производительности предоставляя потокам возможность управлять объектами COM непосредственно (т.е. без использования прокси/заместителей) в силу того, что объекты живут в MTA и потоки также живут в том же MTA.
Другими словами, если есть 50 потоков, входящих в MTA, и один из них создает объект COM (поддерживающий модель MTA), то, по определению модели MTA, все 50 потоков могут непосредственно и одновременно получать доступ к этому одному объекту без необходимости маршалинга указателя на интерфейс объекта из одного потока в другой. Теперь, если Вы оглянетесь назад на Правило #7, то увидите, что я специально указывал, что маршалинг необходим только в случае, когда производится доступ к объекту из одного подразделения в другое, и поэтому Правило #7 остается справедливым и для случая модели MTA, так как множество потоков, живущих в MTA, на самом деле находится в одном подразделении и, следовательно, не происходит кросс-доступ между подразделениями. Правило #8 является гарантией COM для MTA. Эта гарантия означает, что Вы можете держать пари, что COM будет производить все вызовы Вашего объекта в разных потоках в одно и то же время, и, следовательно, Вы должны быть уверены, что Вам удастся удержать целостность Вашего объекта в то время, когда он бомбардируется вызовами, приходящими из различных потоков.
Чтобы выделить это, сформулируем следующее правило:
Правило #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 должен содержать следующий входы системного реестра:
Назначив параметру 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. Я надеюсь, что Вы наслаждались чтением этой статьи так же, как я наслаждался написанием этой статьи (и потоковыми моделями). На этом все. Скоро ждите еще много интересного!
|