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

COM – потоки и контексты

Чистяков В.Ю.

код к статье: ftp://ftp.k-press.ru/pub/cs/2000/4/ComThreading.zip (18 KB)

При реализации модели апартаментов (apartment model) программисты из Microsoft воспользовались своими «окнами». Вернее, не своими окнами, а их очередями сообщений. В оконных очередях сообщений реализован гибкий и производительный механизм обработки поступающих сообщений. Поддерживается как синхронная, так и асинхронная обработка. Rama Krishna (ramakrsna@hotmail.com) предложил неординарный, но очень простой способ показать, как работает апартаментная модель в COM. Суть этого метода заключается в том, чтобы подглядывая за оконными очередями (создаваемыми и используемыми COM) воочию увидеть как работает COM.

COM своими глазами

К сожалению, увидеть окна, которые создает COM, можно только в ОС, вышедших до появления Windows 2000 (W2k). Это связано с тем, что в W2k появились новый тип окон – Message-Only-окна. Такие окна не отображаются утилитой Spy++ (о ней речь пойдет позже). Собственно, Message-Only-окна и есть облегченный вариант окна, имеющий только очередь сообщений и не имеющий ненужных в этом случае графических наворотов. Подводя итог вышесказанному, можно сказать, что этот пример бесполезно пытаться выполнять под управлением W2k, так как вы попросту не увидите окон, которые создает подсистема COM. Несмотря на невидимость окон, пример будет успешно функционировать в W2k, но лучше найти компьютер с установленной Windows NT (NT) 4.0 или Windows 95 и DCOM.

Прежде, чем начать

Для нашего эксперимента нам потребуются:

  1. Visual C++ Version 5.0 или 6.0 (желательно 6.0)
  2. OLE/COM Object Viewer (входит в состав MS Visual Studio (VS), MS Visual C++ (VC) и MS Platform SDK.
  3. Spy++ – Window Spying Utility, поставляемая с VS и VC. 

Прежде, чем двинуться дальше, распакуйте проекты. Разверните файл Comthreading.zip и откройте workspace "COMThreading.dsw" в VC++. Проекты объединены в workspace «COMThreading». Ниже приводится список проектов, входящих в workspace:

Client

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

Server

COM DLL-сервер, реализованный на ATL. Он содержит один объект, поддерживающий единственный простой (не дуальный) интерфейс IServer. Этот интерфейс имеет единственный метод SimpleMethod выводящий окно с сообщением.

ServerPS

Простой Win32 DLL- проект предназначенный для создания proxy-stub DLL из файлов, генерированных компилятором MIDL.

Для начала активизируйте проект Server и скомпилируйте его. В результате будет создана и зарегистрирована Server.dll.

Откройте «OLE/COM Object Viewer» (например, из меню Tools в VC) и посмотрите, что мы теперь имеем.

Проверьте в меню «View OLE/COM Object Viewer’а», включен ли режим «Expert Mode». В ветке «Object Classes» раскройте подветку «All Objects». Найдите в ней подветку «ServerApp Class». Это и есть класс нашего серверного объекта. После этих операций окно «OLE/COM Object Viewer»'а должно выглядеть примерно так, как на рисунке 1. Заметьте, что он показывает только интерфейс IUnknown и никакого IServer. Это потому, что IServer не был распознан в Реестре.

Рис. 1

Первое клиентское приложение

Теперь пора создать клиентское приложение. Чтобы упростить задачу, используем поддержку COM компилятора. С помощью smart pointer'ов, создаваемых компилятором при обработке директивы «#import...» удобнее создать объект ServeApp и вызвать его метод – SimpleMethod. Директива «#import» имеет ряд ключей позволяющих генерировать описание разно степени навароченности. По умолчанию компилятор создает оболочки над каждым обектом, интерфейсом, методом и т.п. Эти оболочки используют структурную модель обработки ошибок C++, избавляя от необходимости напрямую работать с HRESULT-значениями. Они также задают значения по умолчанию параметров методов.

Вот код клиентского приложения.

#include <windows.h>
#pragma hdrstop

#import "Server.tlb" no_namespace
#include "MyComutl.h"

int main(int , char** )
{
   CComInit cominit(COINIT_APARTMENTTHREADED);
   IServerPtr spServer(__uuidof(ServerApp));
   spServer->SimpleMethod();
   return 0;
}

CComInit – класс, производящий инициализацию COM посредством вызова CoInitializeEx в своем конструкторе, и деинициализирующий COM вызовом CoUninitialize в своем деструкторе.

В функции main деструктор IServerPtr (smart pointer, который «держит» указатель на интерфейс серверного объекта и освобождает его при уничтожении, в нашем случае, переменной spServer) вызывается до деструктора CComInit. Как вы понимаете, объявление «IServerPtr spServer(__uuid­of(ServerApp));» создает экземпляр объекта ServerApp, запрашивает у него указатель на интерфейс IServer и сохраняет этот указатель внутри smart pointer'а – spServer. В конце этого нехитрого кода вызывается метод SimpleMethod.

Активизируйте проект «Client», скомпилируйте его, поместите точку прерывания на первую строку (объявление переменной cominit) и запустите проект на выполнение (F5).

Когда выполнение остановится на точке прерывания, откройте Spy++ (например, из меню Tools VC) и в меню Spy выберите пункт Processes. Найдите и раскройте ветку «Client» в дереве процессов (если Spy++ был открыт до этого, и в нем уже было открыто окно Processes, необходимо выбрать его и нажать F5, чтобы обновить информацию). Окно Spy должно выглядеть примерно так, как изображено на рисунке 2.

Рис. 2 .

Обратите внимание, что Spy++ опознал процесс Client. Дочерний элемент ‘XXXXXX D:\xxxxxxx\ComThread­ing\Debug\Client.exe ConsoleWindowClass’, это дескриптор окна, заголовок и имя класса окна консоли нашего клиентского приложения.

Теперь продвинемся в отладчике на строку вперед (F10). После этого вернемся в Spy++ и обновим вид нажатием F5. Теперь дочерние элементы ветки Client выглядят так:

Рис. 3 .

Заметьте, внутри вызова CoInitializeEx(NULL, COINIT_APA­RT­MENTTHREADED) для потока в котором происходил этот вызов было создано новое (скрытое) окно с именем «OleMainThreadWndName» (не забудьте под W2k этого окна видено не будет!). Класс этого окна «OleMainThre­ad­Wnd­Class». Если поток вызывает CoInitialize(NULL) или CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), то создается так называемый Single-Threaded Apartment (однопоточный апартамент), или сокращенно STA. Если же поток вызывает CoInitializeEx(NULL, COINIT_MULTI­THREADED), то он (поток) помещается в так называемый Multi-Threaded Apartment (многопоточный апартамент), или сокращенно MTA.

В одном процессе может существовать сколько угодно STA и только один MTA. Для каждого апартамента (будь то MTA или STA COM создает скрытое окно) это окно (вернее его очередь сообщений) нужно для синхронизации вызовов производимых к апартаменту. Один поток физически не может вызвать другой. Выполнение любого кода в потоке (в том числе и вызовы функций) производится именно в этом потоке, независимо от того, в какой области памяти процесса код располагается, и кем и как он был загружен. Совершенно невозможно из одного потока вызвать функцию из другого потока. Апартаментная модель предполагает, что вызовы к объектам (то есть к их коду) должны производиться из одного из потоков, входящих в этот апартамент. Оконная очередь сообщений нужна как раз для того, чтобы переключаться между потоком, инициирующим вызов, и потоком, закрепленным за апартаментом (Напомню что для STA это единственный поток, а для MTA – любой поток, прикрепленный к MTA.). Такое переключение происходит путем преобразования вызова в оконное сообщение, помещаемое в очередь этого самого скрытого окна. Этим занимается proxy объекта. То есть каждый поток, который хочет вызывать методы у компонента, не входящего в его апартамент, должен иметь указатель не на объект, а на его proxy. Поток, который первым вызывает CoInitialize(NULL) или CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) становится главным STA (Main STA), что отражается в названии созданного окна («OleMainThreadWndName»).

Далее можете или прервать выполнение (Shift+F5) или нормально завершить выполнение, нажав F5...

Клиентское приложение 2 – входим в многопоточный мир

В простом DLL-сервере указатели на интерфейсы могут быть переданы разным потокам, но если DLL создавалась без расчета на многопоточную работу, сбои неизбежны. В некоторых случаях это может привести к трудноуловимым ошибкам, поскольку сбои происходят в непредсказуемые моменты. Как COM-интерфейс, полученный от объекта, созданного в потоке одного апартамента, может быть безопасно использован потоком, входящим в другой апартамент из того же процесса? Ответ COM – «с помощью маршалинга интерфейса из одного потока в другой». Хотя маршалинг привычно (и справедливо) ассоциируется с RPC, в нашем случае маршалинг имеет нечто общее с обеспечением потокобезопасности. Я специально выражаюсь так нечетко – ясность наступит, когда мы разберемся, что происходит на практике.

Чем же именно занимается маршалинг и как это обеспечивает потокобезопасность? Сейчас разберемся...

Чтобы маршалинг работал, нам нужно создать и зарегистрировать proxy/stub dll. Так что скомпилируйте проект ServerPS. При этом автоматически зарегистрируется proxy/stub dll. Чтобы убедиться, что она была зарегистрирована, переключитесь еще раз в «OLE/COM Object Viewer» и обновите информацию о компоненте (например, выберите пункт контекстного меню «Release Instance» и снова откройте этот пункт). Заметьте, что «OLE/COM Object Viewer» показывает интерфейс IServer (см. рисунок 4). Заметьте так же, что описание интерфейса IServer содержит пункт ProxyStubClsid32, значение которого указывает на ключ, приведенный ниже. Это ключ, описывающий Proxy/Stub DLL. Он должен указывать на нашу библиотеку ServerPS. Подробнее о маршалинге и интерфейсов и конкретно о Proxy/Stub можно прочитать в прошлом номере нашего журнала.

Рис. 4 .

Вот и вся подготовка к маршалингу интерфейсов из потока в поток.

Теперь нужно добавить в клиентское приложение некий код для создания нескольких потоков. Этот код находится в файлах mycomutl.cpp и client1.cpp. Все, что нужно – скопировать код из client1.cpp и вставить его в client.cpp, или удалить из проекта client.cpp и добавить client1.cpp – как вам удобнее.

Каждый поток в процессе, нуждающийся в вызовах функций COM должен инициализировать COM, используя CoInitialize(Ex) и деинициализировать его до окончания работы. Для этого нами будет использоваться простая helper-функция BeginCOMThread (ее код находится в mycomutl.cpp):

namespace
{
   struct COMThreadingHelper
   {
      DWORD dwTM; //Потокавая модель (Threading model)
      THREADFN pfnStart;
   };

   unsigned int _stdcall ThreadProc(LPVOID pParam)
   {
      COMThreadingHelper* pCT = reinterpret_cast<comthreadinghelper*>(pParam);
      
      //Инициализируем COM для этого потока
      CComInit cominit(pCT->dwTM);
      
      //Вызываем пользовательскую функцию 
      (pCT->pfnStart)();

      delete pCT;
      
      return 0;
   }
}
HANDLE BeginCOMThread(DWORD dwTM, THREADFN pfnStart)
{
   unsigned int dwId;
   
   COMThreadingHelper* pCT = new COMThreadingHelper;

   pCT->dwTM = dwTM;
   pCT->pfnStart = pfnStart;
   
   return (HANDLE) _beginthreadex(
                  NULL,         //Атрибут защиты
                  0,            //Размер стека
                  ThreadProc,   //Адрес главной процедуры потока
                  (void*)(pCT), //Параметр передающийся в ThreadProc
                  0,            //0 – Запускать сразу после создания
                  &dwId         //[out] идентификатор потока
                  );
}

Определение COMThreadingHelper и ThreadProc находятся в безымянном namespace. Этот способ применяется в С++, чтобы сделать функции и структуры локальными.

Стоит упомянуть, что вместо Win32-функции CreateThread используется функция из С-runtime библиотеки _beginthreadex. Причина этого в том, что C-runtime поддерживает специфичные для потока глобальные данные, например, errno и т.д. Если вы намерены использовать функции из С-runtime, то потоки необходимо создавать именно таким образом, если нет (например, вы пользуетесь только Win32-API и ATL), можно использовать функцию CreateThread.

Итак, у нас есть функция, создающая поток и инициализирующая COM для него. Для того чтобы произвести маршалинг указателя на интерфейс IServer из одного потока в другой, мы воспользуемся одной из самых длинных (в смысле имени) функций OLE32 API CoMarshalInterThreadInterfaceinStream. Теперь функция main выглядит так:

int main(int , char** )
{
   CComInit cominit(COINIT_APARTMENTTHREADED);

   IServerPtr spServer(__uuidof(ServerApp));
   spServer->SimpleMethod();

   //маршалим (вручную!) указатель на интерфейс IServer
   //находящийся в spServer
   HRESULT hr = CoMarshalInterThreadInterfaceInStream(
      __uuidof(IServer), //IID интерфейс, подвергающегося маршалингу
      spServer, //Указатель на интерфейс
      &g_pStm   //Указатель на IStream куда помещается бинарное описание 
                //интерфейса
   );
   
   //Запускам STA-поток
   HANDLE hThread = BeginCOMThread(COINIT_APARTMENTTHREADED, AThread);
   
   //Ожидание окончания выполнения потока (Дескриптор потока переходит в 
   //сигналящее состояние когда поток завершает свое состояние
   WaitForSingleObject(hThread, INFINITE);
   
   //_endthreadex не закрывает дескриптора в отличие от _endthread 
   //Когда исполнение функции, вызванной из _beginthreadex, 
   //завершается,_endthreadex вызывается автоматически. Тем не менее нам нужно
   //закрыть дескриптор потока...
   CloseHandle(hThread);

   return 0;
}

Глобальная переменная g_pStm типа «указатель на IStream» используется функциями COM для маршалинга интерфейса между потоками.

Чтобы основное приложение не закончило работу раньше созданного нами потока, нам следует использовать функцию WaitForSingleObject.

Функция AThread, получает (с помощью функции CoGetInterfaceAndReleaseStream) указатель на интерфейс IServer и производит вызов метода IServer::SimpleMethod у объекта созданного в основном потоке приложения. Вот ее код:

//Эта функция выполняется в отдельном потоке
void AThread()
{
   IServerPtr spServer;
   HRESULT hr = CoGetInterfaceAndReleaseStream(g_pStm, 
                           __uuidof(IServer), 
                           reinterpret_cast<LPVOID*>(&spServer));
   
   spServer->SimpleMethod();
}

Поставьте точку прерывания в начале функции AThread. Запустите отладчик. Как только выполнение программы остановится на этой точке, откройте Spy++ и еще раз посмотрите на клиентский процесс (если Spy++, то не забудьте обновить информацию). На этот раз там должно быть нечто такое:

Рис.5.

Как вы видите Spy++ показывает оба потока.

Пройдите в пошаговом режиме до вызова SimpleMethod. Заметьте, что после вызова CoGetInterfaceAndReleaseStream значение указателя spServer установлено в некоторое значение. Продвиньтесь еще на одну строку. Что происходит? Приложение зависло. Все, что можно сделать, это выбрать «stop debugging» из меню Debug (или нажать Shift+F5).

Если бы вызов spServer->SimpleMethod был прямым (без маршалинга) вызовом функции из dll, приложение не повисло бы. Но мы провели маршалинг интерфейса в другой поток и теперь используем его в другом потоке. Вызов spServer->SimpleMethod – уже не прямой вызов, он должен пройти системный код, прежде чем достигнет реальной функции SimpleMethod в Server.dll.

Поскольку указатель на интерфейс, полученный в результате маршалинга, на самом деле соответствует объекту, созданному в другом потоке, а объект не может работать с несколькими потоками, функция SimpleMethod объекта должна вызываться из потока, где создавался объект. Как уже говорилось ранее, вызывающий поток должен как-то сообщить потоку из апартамента, в котором был создан объект, что нужно вызвать его функцию SimpleMethod. Контекст исполнения следует изменить. Как это сделать? Это несложно, если вспомнить скрытое окно, созданное потоком, ассоциированным с объектом. Все, что нужно вызывающему потоку – так это послать сообщение в скрытое окно с помощью SendMessage. Как только оконное сообщение приходит по назначению, поток, которому принадлежит объект, прочтет это сообщение и преобразует данные, присланные с этим сообщением, в стековый фрейм и вызовет необходимый метод. Заметьте, вызов метода будет осуществлен из потока, в котором объект и был создан, то есть, в нашем случае, из главного потока приложения. Но мы не реализовали никаких циклов обработки сообщений в основном потоке приложения, вместо этого мы заморозили поток, переведя его в режим ожидания завершения исполнения другого потока. Это тупик.

Мы должны реализовать цикл сообщений в основном потоке. Точнее, нам нужно направить оконные сообщения соответствующим процедурам окна. Но нам нужно также дождаться конца работы созданного нами потока. К счастью, Win32 API предоставляет для этого функцию MsgWaitForMultipleObjects , срабатывающую при наличии сообщений в очереди сообщений потока или при сигнале от объектов. Детальное описание этой функции вы можете найти в MSDN.

Нам нужно изменить основную функцию, включив в нее диспетчеризацию сообщений. Функцию WaitForSingleObject нужно заменить следующим кодом (можно взять его в Client2.cpp или просто подключить Client2.cpp к проекту вместо Client1.cpp):

//Ждем окончания работы потока

   //Дескриптор потока переходит в сигналящее состояние если потока завершил
   //свою работу
   for(;;)
   {
      DWORD dwRet = MsgWaitForMultipleObjects(
          1,        //Счетчик объектов ядра (в нашем случае он один)
          &hThread, //Указатель на первый элемент массив объектов ядра
         FALSE,     //Ждать все объектыWait (в нашем случае он один)
         INFINITE,  //Сколько ждать? (в нашем случае до посинения)
         QS_ALLINPUT//Какие сообщения ждать (мы ждем все сообщения)
      );
      
      //Если поток закончил свою тоботу...
      if(dwRet != WAIT_OBJECT_0 + 1)
         break; //то выходим из цикла

      //Удалить сообщения из очереди
      MSG msg;
      while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0)
      {
         //Несущественно
         TranslateMessage(&msg);
         //Доставить сообщение по назначению
         DispatchMessage(&msg);
      }
   }
   ...

Поместив точку прерывания в функцию AThread, снова запустите программу в отладчике. Когда выполнение прервется, откройте Spy++ и раскройте ветку «Client». Она будет выглядеть практически так же, как и в предыдущий раз. Теперь поставьте точку прерывания в методе CServerApp::Simple­Method. Он находится в файле ServerApp.cpp проекта Server и позвольте отладчику продолжить исполнение. Когда выполнение остановится на этой точке, посмотрите на стек вызовов. Стек вызовов будет выглядеть примерно так:

Рис.6 .

Как следует из стека вызовов, в эту точку мы попали через методы Kernel32.dll, rpcrt4.dll и ole32.dll, а не напрямую из клиентского приложения. Это и есть работа маршалинга. Вызов метода даже для in-proc сервера должен идти через код системного уровня. Все это делается для обеспечения потокобезопасности методов компонента.

Теперь, оставив точку прерывания на прежнем месте в функции AThread, перезапустите приложение в отладчике. После CoGetInterfaceAndReleaseStream снова откройте Spy++. На этот раз вы увидите нечто типа:

Разница в том, что теперь и основной поток, и созданный нам содержат скрытые окна. Во втором потоке имеется скрытое окно того же класса, но с другим названием – «OLEChannelWnd». Будет полезно проверить, какие сообщения посылаются каждому из окон. Spy++ снова поможет нам, щелкните правой кнопкой мыши по каждому из окон и выберите «messages» из контекстного меню. Теперь все сообщения, поступающие в эти окна, будут показаны в Spy++.

Рис. 7 .

Позволим приложению завершить работу. Теперь вернемся в Spy++ и посмотрим на лог сообщений:

Рис. 8 .

Обратите внимание на два WM_USER-сообщения. В wParam этих сообщений можно мгновенно узнать дескриптор другого потока. Итак, становится ясным, что COM посылает сообщения WM_USER скрытому окну, ассоциированному с потоком, к которому принадлежит объект, в случае, если метод объекта вызывается из другого потока через «отмаршаленный» интерфейс. Все это делает код маршалинга, находящийся в основном в rpcrt4.dll.

Итак, апартамент это логическая группа, состоящая из одного или более потоков, для которой при вызове метода из потока, не входящего в эту группу (другого апартамента), COM производит маршалинг. Маршалинг подразумевает сериализацию параметров и переключение потока исполнения, посредством очередей сообщений специальных скрытых окон, в чем мы только что убедились.

Дотошный читатель может задаться вопросом, а как обойти радушную помощь COM, если ваш компонент и без COM готов к многопоточной работе, да еще и делает это более быстро и эфективно?

В W2k для этого введена новая потоковая модель – Neutral, а в предыдущих версиях Windows можно пользоваться специальным объектом FreeThreaded Marshaler (FTM) помечая компонент как поддерживающий модель Both (то есть Apartment и Free одновременно). Both позволяет объекту создаваться в любом (Apartment или Free) апартаменте, а агрегация FTM – избегать создания межпоточного proxy. Недостатков у объектов, агрегирующих FTM (далее – FTM-объектов), два. Первый – такие объекты не могут принадлежать к «COM+»-контексту (о нем речь пойдет далее), второй – это то, что такой объект не может хранить указатель на интерфейсы других объектов, если только эти объекты тоже не агрегируют FTM. Но первый недостаток может быть и достоинством. Дело в том, что при попытке запросить «COM+»-контекст будет возвращен контекст апартамента, из которого был вызван метод FTM объекта. Это дает возможность создавать объекты, выполняющие действия «от имени и по поручению» других объектов. Второй недостаток обходится с помощью ручного явного маршалинга указателей на интерфейс (так как мы это уже делали в этой статье) или с помощью GIT. Зато FTM-объекты дают значительный прирост производительности, ведь их методы вызываются напрямую (без proxy), и при вызове не происходит переключения потоков.

Neutral-модель – это более сложный механизм, позволяющий получить отдельный «COM+»-контекст, и при этом, так же, как и в случае с FTM-объектами, не производить переключения потоков. Более подробно Neutral-модель будет разобрана далее.

Как же задается потоковая модель? Потоковая модель определяется для компонента (CoClass'а) в реестре Windows. В HKEY_CLASSES_ROOT\CLSID\CLSID конкретного компонента \InprocServer32 может иметься значение с именем ThreadingModel. Если оно отсутствует, то компонент может создаваться только в Main STA (см. выше). Если значение присутствует, то оно может содержать следующие значения:

Значение

Где создается компонент

отсутствует

в Main STA

Apartment

в STA (Single-Threaded Apartment)

Free

в MTA (Multi-Threaded Apartment)

Neutral

в NTA (Neutral-Threaded Apartment. Поддерживается начиная с W2k)

Both

в апартаменте активатора (т.е. в любом)

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

Интересный эффект можно наблюдать, если изменить потоковую модель нашего компонента Server на Neutral (Это эксперимент можно проделать только под W2k).

При этом «OLE/COM Object Viewer» начинает показывать забавную картину (см. рисунок 9).

Рис. 9.

Дело в том, что «OLE/COM Object Viewer» явно выводит в списке не описанный в библиотеке типов список поддерживаемых интерфейсов, а пробует получить это список динамически (видимо, просматривая список интерфейсов, зарегистрированных в реестре, и пытаясь их запросить у объекта).

Появившиеся дополнительные интерфейсы относятся к защите (IClientSecurity) и к управлению маршалингом (все остальные). По всей видимости, код COM обнаруживает, что создаваемый объект помечен как поддерживающий Neutral-апартамент и агрегирует его с некоторой оберткой, эмулирующей нечто вроде MBV (см. прошлый номер) и кодом, управляющим защитой.

Если теперь поставить точку прерывания в методе SimpleMethod, то мы получим call-стеки, показанные ниже:

  1. При вызове из того же потока:

    Обратите внимание на то, что появилось большое количество промежуточных вызовов в модулях rpcrt4 и ole32.

  2. При вызове из другого потока:

А во втором случае интересно не обилие промежуточных вызовов (их и раньше было немало), а то, что, в отличие от времени, когда компонент был зарегистрирован как поддерживающий поточную модель Apartment, контекст потока не переключается. Выполнение происходит в том же потоке!

При вызове методов из другого апартамента блокируется не только выполняемый в этот момент код, но и пользовательский интерфейс. Но эта блокировка не полная и ее можно управлять. Это происходит с помощью интерфейса IMessageFilter.

далее>>
 

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