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

<< назад

IMessageFilter – блокировка GUI при COM-вызовах

Для управления блокировкой интерфейса Windows предоставляет интерфейс IMessageFilter . Он позволяет COM-серверам и приложениям, взаимодействующим с этими серверами, выборочно обрабатывать Windows-сообщения во время ожидания ответов на синхронные COM-вызовы.

Синхронные вызовы требуют, чтобы вызывающая сторона ожидала ответа перед продолжением своей работы. При ожидании ответа COM входит в модальный цикл. В это время вызывающая сторона все ещё может получать и обрабатывать входящие сообщения.

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

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

Вызовы объектов из главного потока приложения практически всегда синхронизированы по вводу. При обращению к удаленному объекту COM заблокирует большинство сообщений. По умолчанию будут обрабатываться такие сообщения, как WM_PAINT. Окна при этом не будут откликаться на ввод пользователя, и ему может показаться, что приложение зависло. Если пользователь будет настойчив, то умолчальная реализация интерфейса IMessageFilter (встроенная в Windows) выдаст примерно такое диалоговое окно, как показано на рисунке 10.

Надо признаться – «кривоватая вещица». Этот диалог был создан для OLE2-приложений. Для распределенных COM-приложений он подходит не очень. Особо глупо выглядит кнопка «Switch To...». Тяжело переключится на приложение, работающее на другом компьютере, да ещё к тому же не имеющее GUI. Да ещё и красивая кнопочка «Cancel», как будто издеваясь, всем своим видом говорит – «Что, нажал?!».

Рис. 10.

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

Все эти проблемы можно обойти, если реализовать свою обработку сообщений. Сделать это можно, создав свой COM-объект, реализующий интерфейс IMessageFilter и заменив (с помощью API-функции CoRegisterMessageFilter ) стандартную обработку. Создавать такой объект лучше на C/C++, ввиду его низкоуровневости, но можно попытаться сделать это и на Delphi.

Когда создать свой обработчик?

Реализация по умолчанию, предоставляемая СОМ, предлагает лишь минимальные возможности фильтрации сообщений. Впрочем, фильтрация сообщений теперь уже не так важна, как во времена 16-битных приложений (размер очереди сообщений теперь практически не ограничен).

COM будет вызывать вашу реализацию IMessageFilter, чтобы выяснить, заблокировано ли это приложение, и чтобы дать вам возможность отменить вызов или продолжить ввод в безопасные, с точки зрения удаленного вызова, поля вашего приложения. Например, вы можете вообще отказаться от выдачи диалогового окна. Вместо этого можно реализовать кнопку «отменить» и поместить ее в строку состояния вашего приложения.

Правда, есть одна проблема. При отмене сообщения важно, рассчитан ли на это удаленный объект. Если объект создан в STA, то после того, как вы в первый раз прервете выполнение некоторого метода, может статься так, что компонент не будет доступен для других вызовов. Дело в том, что «прерывание» метода не значит реального его останова. COM просто прекращает модальный цикл и передает управление вызывающей стороне, а метод удаленного компонента продолжает упорно трудиться, хотя его труд уже ни кому больше не нужен. Полностью эту проблему можно разрешить только под W2k (подробней смотри в номере нашего журнала за 2 квартал нашего года). В этой ОС имеется более продуманный подход к управлению требованиями отмены, исходящими из любого потока вызова. При маршалинге интерфейса создается proxy с встроенным cancel-объектом, реализующим интерфейс ICancelMethodCalls . Этот объект ассоциирован с вызовом и с потоком, где задерживается вызов. Но и под W2k для того, чтобы метод остановился, он сам должен проверить состояние вызова, что требует дополнительного кода. К тому же может сложиться ситуация, когда сам удалённый метод заблокирован. Например, он ожидает окончания вызова к другому COM-объекту, или он сделал не очень разумный запрос к БД и теперь безуспешно пытается дождаться ответа. Как бы там ни было, это тупик. Так что если вы предполагаете, что вызов метода может «зависнуть», то лучше сделать такой компонент многопоточным. При этом повисший метод может себе «висеть», а вы сможете спокойненько выполнить ещё один вызов, ведь все вызовы к компоненту происходят в разных потоках и могут происходить параллельно. Если вы выбрали такую стратегию, то не забудьте убедиться, что для этого компонента отключена синхронизация. Да и вообще надо быть аккуратней, ведь данные такого компонента придется защищать вручную. Желательно дать возможность дать возможность коду, производящему удаленный вызов, оповещать реализацию вашего фильтра, можно ли отменять вызов данного метода.

Но вернемся к реализации нашего обработчика... Хотя это, скорее всего, очевидно из описаний методов, стоит, видимо, подчеркнуть, что HandleIncomingCall  – метод, предназначенный для разгребания сообщений внутри процесса объекта, а RetryRejectedCall и MessagePending  – методы, предназначенные для работы на вызывающей стороне. Ясно, что объект должен располагать каким-то способом обработки вызовов, приходящих от удаленных клиентов. HandleIncomingCall предоставляет такую функциональность, позволяя объекту обрабатывать или откладывать обработку некоторых входящих вызовов, и отказываться от остальных. Клиент также должен знать, как объект собирается обойтись с его вызовом, чтобы реагировать соответственно ситуации. Клиенту, чтобы повторить вызов после некоторой паузы, нужно знать, отвергнут вызов объектом, или его обработка просто временно отложена. Клиент должен также уметь отвечать на сообщения Windows, одновременно ожидая ответов на задержку с ответом от сервера.

И так как я уже говорил, для регистрации фильтра сообщений надо использовать API-функцию CoRegisterMessageFilter . Вам нужно создать экземпляр вашего объекта, запросить у него указатель на IMessageFilter и передать его функции CoRegisterMessageFilter. После регистрации COM будет вызывать ваш фильтр сообщений вместо реализации, используемой по умолчанию. Напрямую этот интерфейс вызывать нет необходимости, это будет делать Windows.

Завершение работы приложений при WM_QUERYENDSESSION и WM_ENDSESSION

При завершении работы Windows каждое открытое приложение получает сообщение WM_QUERYENDSESSION, сопровождаемое сообщением WM_ENDSESSION, в случае, если закрытие сессии не отменено. Эти сообщения посылаются с помощью SendMessage , к сожалению, при этом запрещаются инициализацию все исходящие LRPC-вызовов. При создании серверного приложения следует отдельно протестировать поведение приложения при закрытии сессии и выключении компьютера.

Таблица 1. Методы в порядке Vtable

Методы IMessageFilter Описание

HandleIncomingCall

Предоставляет единую точку входа для входящих вызовов. Используется для отмены или откладывания обработки вызовов на серверной стороне.

RetryRejectedCall

Дает приложению возможность вывода диалогового окна с предложением повтора, отмены или переключения задач (последний случай используется исключительно при взаимодействии с локальными приложениями типа Excel).

MessagePending

Сообщает о появлении Windows-сообщения, в момент, когда COM ожидает ответа от сервера.

Серверные приложения должны возвращать WM_QUERY­ENDSESSION TRUE без оповещения пользователя. При получении сообщения WM_ENDSESSION все COM-приложения должны выполнять нормальную процедуру закрытия каждого объекта приложения. В то же время, следует игнорировать любые ошибки, возникающие в результате межпроцессных вызовов или вызовов IUnknown::Release. Все указатели хранилищ (указатели интерфейсов IStorage и IStream) должны быть освобождены для корректного удаления любых временных файлов.

Метод HandleIncomingCall нам мало интересен потому, что, во-первых, он относится только к серверу, а во-вторых, он не позволяет отменить уже работающий вызов. Метод RetryRejectedCall вообще работает, если вы работаете с локальными OLE-приложениями, а вот MessagePending может быть нам очень интересен. Исходя из этого я дам только короткое описание методов HandleIncomingCall и RetryRejectedCall, а подробнее остановлюсь на методе MessagePending.

DWORD HandleInComingCall(
  //Тип входящего вызова
  DWORD dwCallType, 
  //Дескриптор задачи, вызывающей данную задачу
  HTASK threadIDCaller,
  //Прошедшее количество тиков
  DWORD dwTickCount,
  //Указатель на структуру INTERFACEINFO содержащую 
  //описание вызываемого интерфейса, объекта и метода
  LPINTERFACEINFO lpInterfaceInfo 
);
Возвращаемое значение
DWORD RetryRejectedCall(
   HTASK threadIDCallee, 	//Дескриптор серверной задачи
   DWORD dwTickCount, 	//Прошедшее количество тиков
   DWORD dwRejectType	//Возвращенное сообщение об отказе
);

Примечание : Если клиент реализует IMessageFilter и вызывает серверные методы на удаленной машине, RetryRejectedCall не вызывается.

IMessageFilter:: MessagePending

Клиентский метод, вызываемый COM при появлении сообщения Windows в очереди сообщений COM-приложения в процессе ожидания приложением окончания удаленного вызова. Обработка ввода в процессе ожидания завершения исходящего вызова может представлять трудности. Приложение должно определить, обрабатывать поступившее сообщение (не прерывая процесса ожидания возврата), продолжить ожидание или отменить ожидание.

DWORD MessagePending(
//Дескриптор задачи вызываемого приложения
  HTASK threadIDCallee,
  DWORD dwTickCount,     //Прошедшее количество тиков 
  DWORD dwPendingType    //Тип вызова Call type
);
Параметры
Возможные возвращаемые значения
Примечания

COM вызывает IMessageFilter::MessagePending после того, как приложение сделало вызов COM-метода удаленного объекта, и получило сообщение Windows до возврата управления. Windows-сообщение может быть послано, например, когда пользователь выбирает команду меню или проводит мышью над некоторым окном. До того, как COM сделает вызов IMessageFilter::MessagePending , он подсчитывает время, прошедшее с момента совершения вызова метода удаленного объекта. Это время передается в параметре dwTickCount . В момент вызова метода MessagePending сообщение лежит в очереди сообщений приложения.

Сообщения Windows, появляющиеся в очереди вызывающей стороны, должны оставаться в очереди, пока не пройдет достаточно времени, чтобы убедиться, что это не результат опережающего ввода, а наоборот, попытка привлечь внимание. Используйте параметр dwTickCount для отслеживания задержки. Рекомендуется не менее чем двух- или трехсекундная задержка. В системах до W2k эта задержка была неприлично долгой и если вы, например, ввели неправильное имя удаленного компьютера, то можно было прождать несколько минут. Для обычного пользователя это неприемлемо долго! Так что даже только для изменения этой задержки имеет смысл сделать свой обработчик. Если время истекло, а вызов ещё не окончен, вызывающей стороне следует удалить сообщения из очереди, и показать диалоговое окно, предлагающее пользователю повторить вызов (ждать дальше) или отменить вызов (уведомив его об опасности этой операции).

В качестве диалога можно использовать стандартный диалог (OleUIBusy), но лучше создать собственный (уж больно убог стандартный).

Кое-какая обработка IMessageFilter:: Message­ Pending сделана в MFC. Она не совершенна, но может послужить затравкой для вашей реализации. Вот она:

STDMETHODIMP_(DWORD) COleMessageFilter::XMessageFilter::MessagePending(
   HTASK htaskCallee, DWORD dwTickCount, DWORD /*dwPendingType*/)
{
   METHOD_PROLOGUE_EX(COleMessageFilter, MessageFilter)
   ASSERT_VALID(pThis);

   MSG msg;
   if (dwTickCount > pThis->m_nTimeout && !pThis->m_bUnblocking &&
      pThis->IsSignificantMessage(&msg))
   {
      if (pThis->m_bEnableNotResponding)
      {
         pThis->m_bUnblocking = TRUE;    // avoid reentrant calls

         // eat all mouse messages in our queue
         while (PeekMessage(&msg, NULL, WM_MOUSEFIRST, WM_MOUSELAST,
            PM_REMOVE|PM_NOYIELD))
            ;
         // eat all keyboard messages in our queue
         while (PeekMessage(&msg, NULL, WM_KEYFIRST, WM_KEYLAST,
            PM_REMOVE|PM_NOYIELD))
            ;

         // show not responding dialog
         pThis->OnNotRespondingDialog(htaskCallee);
         pThis->m_bUnblocking = FALSE;

         return PENDINGMSG_WAITNOPROCESS;
      }
   }

   // don't process re-entrant messages
   if (pThis->m_bUnblocking)
      return PENDINGMSG_WAITDEFPROCESS;

   // allow application to process pending message
   if (::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE|PM_NOYIELD))
      pThis->OnMessagePending(&msg);

   // by default we return pending MSG wait
   return PENDINGMSG_WAITNOPROCESS;
}

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

CoRegisterMessageFilter

Регистрирует в OLE объект, реализующий интерфейс IMessageFilter. Такая регистрация делается одна на весь процесс (EXE-модуль). Вопреки заверениям документации Microsoft, регистрировать обработчик можно и из DLL-модуля, но замечание о регистрации на весь процесс остается в силе.

Альтернативный подход

Чтобы обойти блокировку GUI, можно просто выполнять вызовы из другого потока, не связанного с пользовательским интерфейсом. При этом надо не забывать о маршалинге интерфейсов. Конечно, каждый вызов бессмысленно делать из отдельного потока. Имеет смысл делать это с вызовами, при которых велика вероятность больших задержек или подвисаний. При работе с БД имеет смысл задавать таймаут для выполнения SQL-запросов. Бросая выполняющийся вызов, надо всегда помнить о том, что «подвисший» вызов может занимать большое количество ресурсов (памяти, процессорного времени, ресурсов и т.п.).

Итак, мы разобрались, что такое апартамент, с чем его едят, и как им управлять. Вы заметили, что по ходу дела то тут, то там встречались замечания насчет изменений, вносимых в СОМ в W2k. В следующем разделе мы подробным образом рассмотрим изменения, вносимые W2k в потоковые модели COM и наконец-то узнаем – «что такое контекст ? ».

далее>>


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