вторник, 28 сентября 2010 г.

Использование OmniThread Libray (OTL) для создания многопоточных приложений - 6


Обработка сообщений Windows в OTL.

Итак, в предыдущих разделах мы научились работать  с сообщениями. Однако эти сообщения были заданы нами. В данном же разделе мы рассмотрим работу с сообщениями Windows, научимся перехватывать и обрабатывать их в фоновых задачах.

Рассмотрим как, например, с помощью OTL можно сделать простую синхронную обертку вокруг асинхронного компонента. Представленный ниже код не входит в демонстрационные примеры OTL. Данный код представлен автором библиотеки OTL в своем блоге и показывает реализацию взаимодействия сообщений Windows c OTL. Итак, что будет делать код? Данный будет загружать веб-страницу с интернета. Казалось бы, что проще – можно использовать Indy, можно WinInet - эти библиотеки синхронны, но автор OTL использует ICS, которая является асинхронной библиотекой.
Для читателей, которые не знакомы с терминами синхронности и асинхронности кода сделаем краткое отступление:
Синхронной считается код, который выполняется строго последовательно. Например, код загружающих веб-страницу с интернета не передаст управление далее, а будет ждать завершения загрузки. Асинхронный код, делающий такую-же операцию немедленно передаст управление следующему оператору, даже если страница еще не загрузилась. Сама же загрузка в это время будет идти где-нибудь на заднем плане. По окончании загрузки страницы программа будет оповещена, что страница загружена посредство сообщения. Хотя второй путь и является более гибким, позволяет Вам делать больше вещей параллельно, не блокирует GUI, но является более сложным в понимании и реализации.
Следующие функции сделаны как можно более простыми для понимания. Функция GpHttpGet производит скачивание, а GpHttpPost делает запрос. На входе в параметрах обе функции принимают, имя пользователя и пароль. В возвращаемых переменных функции передают код статуса возврата (200-  если все пошло хорошо), текст статуса возврата и содержание страницы. В случае проблемы WinSock - возвращается код ошибки в statusCode. (Коды ошибок HTTP всегда ниже 1 000, и коды WinSock всегда выше 10 000, таким образом, нет никакой возможности для неправильного толкования кода ошибки)
Обе функции - простые обертки вокруг третьей функции, которая выполняет любой запрос HTTP.

function GpHttpGet(const url, username, password: string;
  var statusCode: integer; var statusText, pageContents: string): boolean;
begin
  Result := GpHttpRequest(url, username, password, 'GET', '', statusCode,
    statusText, pageContents);
end; { GpHttpGet }

function GpHttpPost(const url, username, password, postData: string;
  var statusCode: integer; var statusText, pageContents: string): boolean;
begin
  Result := GpHttpRequest(url, username, password, 'POST', postData,
    statusCode, statusText, pageContents);
end; { GpHttpPost }

Реальная работа начинается в методе GpHttpRequest. Сначала этот метод создает фоновую задачу worker (от IOmniWorker), в которой и происходит запрос данных с интернета. Ссылка на объект (или более правильно на интерфейс, поддерживаемый этим объектом) будет сохранена в переменной. Эта переменная будет нам нужна в конце, чтобы возвратить код статуса и содержание страницы.
Далее мы будем ожидать завершение задачи. Время ожидания мы выставим в 30 секунд  (вы можете легко изменить константу CGpHttpRequestTimeout_sec в коде для выставления другого времени).
Если задача не будет закончена через 30 секунд, то мы принудительно закончим ее и возвратим False. Иначе, мы вернем код состояния, текст и содержание страницы.
GpHttpRequest
Const
 CGpHttpRequestTimeout == 30;

function GpHttpRequest(const url, username, password, request,
  postData: string; var statusCode: integer; var statusText,
  pageContents: string): boolean;
var
  task  : IOmniTaskControl;
  worker: IOmniWorker;
begin
  // TGpHttpRequest – класс от TOmniWorker
  // MsgWait указывает фоновой задаче, что нужно обрабатывать Windows-сообщения
  // внутри фонового потока
  worker := TGpHttpRequest.Create(url, username, password, request, postData);
  // Создаем задачу
  task := CreateTask(worker, 'GpHttpRequest').MsgWait.Run;
  // Ждем ее выполнения
  Result := task.WaitFor(CGpHttpRequestTimeout_sec * 1000);
  if not Result then
     // Неудачно завершилась - выход
    task.Terminate
  else
  begin
     // Удача – приводим интерфейс к объекту и возвращаем данные
    statusCode   := TGpHttpRequest(worker.Implementor).StatusCode;
    statusText   := TGpHttpRequest(worker.Implementor).StatusText;
    pageContents := TGpHttpRequest(worker.Implementor).PageContents;
  end;
end; { GpHttpRequest }

В методе инициализации (Initialize) класса реализующего выполнение фоновой задачи впишем следующий код:
TGpHttpRequest.Initialize
function TGpHttpRequest.Initialize: boolean;
begin
  hrHttpClient := THttpCli.Create(nil);
  try
    hrHttpClient.NoCache := true;
    hrHttpClient.RequestVer := '1.1';
    hrHttpClient.URL := hrURL;
    hrHttpClient.Username := hrUsername;
    hrHttpClient.Password := hrPassword;
    hrHttpClient.FollowRelocation := true;
    if hrUsername <> '' then
      hrHttpClient.ServerAuth := httpAuthBasic;
    hrHttpClient.SendStream := TStringStream.Create(hrPostData);
    hrHttpClient.RcvdStream := TStringStream.Create('');
    // Обработчик вызываемый при завершении скачки
    hrHttpClient.OnRequestDone := HandleRequestDone;
    if SameText(hrRequest, 'GET') then
      hrHttpClient.GetASync
    else if SameText(hrRequest, 'POST') then
      hrHttpClient.PostASync
    else
      raise Exception.CreateFmt(
        'TGpHttpRequest.Initialize: Unknown request type %s',
        [hrRequest]);
    Result := true;
  except
    on E:ESocketException do begin
      hrStatusCode := -1;
      hrStatusText := E.Message;
      Result := false;
    end;
  end;
end; { TGpHttpRequest.Initialize }

Код создает объект THttpCli. THttpCli - класс HTTP-клиента ICS, используемый, чтобы выполнить запросы HTTP асинхронно или синхронно. Здесь автор использует асинхронный способ, потому что синхронная функция загрузки THttpCli.Get не имеет параметр времени ожидания и вызывает в своей работе Application.ProcessMessages, который не может быть использован в фоновом потоке. Далее код настраивает параметры THttpCli и вызывает асинхронные функции библиотеки ICS GetASync или PostASync.
Если GetASync или PostASync генерируют во время работы исключение, то код генерирует новое исключение и передает код статуса и текст возвращения. По этому исключению главный цикл IOmniWorker оповещается о недопустимости возобновления выполнения задачи.
Итак, мы достигли асинхронной работы. ICS работает на заднем плане, и все другие фоновые задачи (нити) только ждут завершения его работы.
Поскольку архитектура ICS базируется на сообщениях, кто- то должен обработать сообщения Windows, которые идут через ICS непосредственно.
Именно поэтому мы должны были использовать MsgWait, при создании фоновой задачи в CreateTasks. Это позволит нам задействовать локальную петлю сообщений OTL в нашем потоке при обработке сообщений Windows.
Спустя некоторое время THttpCli полностью загрузит веб-страницу и вызовет обработчик события HandleRequestDone. Здесь мы только скопируем соответствующие данные во внутренние переменные и затем закончим фоновую задачу.
TGpHttpRequest.HandleRequestDone  Обработчик Windows сообщения
procedure TGpHttpRequest.HandleRequestDone(sender: TObject;
  rqType: THttpRequest; error: word);
begin
  if error <> 0 then begin
    hrStatusCode := error;
    hrStatusText := 'Socket error';
  end
  else begin
    hrPageContents := TStringStream(hrHttpClient.RcvdStream).DataString;
    hrStatusCode := hrHttpClient.StatusCode;
    hrStatusText := hrHttpClient.ReasonPhrase;
  end;
  Task.Terminate;
end; { TGpHttpRequest.HandleRequestDone }

В методе Cleanup вызываемой OTL при завершении фоновой задачи  выполним  подчистку за собой объектов ICS
TGpHttpRequest.Cleanup
procedure TGpHttpRequest.Cleanup;
begin
  if assigned(hrHttpClient) then begin
    hrHttpClient.RcvdStream.Free;
    hrHttpClient.SendStream.Free;
    FreeAndNil(hrHttpClient);
  end;
end; { TGpHttpRequest.Cleanup }

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

function TOmniTaskControl.MsgWait(wakeMask: DWORD): IOmniTaskControl;
begin
  Options := Options + [tcoMessageWait];
  otcExecutor.WakeMask := wakeMask;
  Result := Self;
end; { TOmniTaskControl.MsgWait }

Как Вы видите, функция просто выставляет флаги работы и возвращает указатель на собственный объект. Непосредственно вся тяжелая работа сделана в TOmniTaskExecutor.Asy_DispatchMessages. Если флаг tcoMessageWait выбор будет установлен, то внутри локальной петли сообщений OTL функция MsgWaitForMultipleObjectsEx будет также ждать сообщения Windows (в дополнение ко всем остальным), потому что получит непустой указатель waitWakeMask. Когда такое сообщение будет обнаружено, код вызовет метод ProcessThreadMessages, который просто извлечет сообщение из очереди и пошлет сообщение Windows в оконную процедуру Delphi, после чего внутренний механизм отправки сообщений Delphi позаботится обо всем остальном.
Внутренняя кухня обработки сообщений OTL
if tcoMessageWait in Options then
  waitWakeMask := WakeMask
else
  waitWakeMask := 0;
//...
awaited := MsgWaitForMultipleObjectsEx(numWaitHandles, waitHandles,
  cardinal(timeout_ms), waitWakeMask, flags);
//...
else if awaited = (WAIT_OBJECT_0 + numWaitHandles) then //message
  ProcessThreadMessages

procedure TOmniTaskExecutor.ProcessThreadMessages;
var
  msg: TMsg;
begin
  // Извлечь сообщение из очереди (петля сообщений)
  while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) and
                   (Msg.Message <> WM_QUIT) do
  begin
    TranslateMessage(Msg); // трансляция клавиатурных сообщений
    DispatchMessage(Msg);  // посылаем в оконную процедуру
  end;
end; { TOmniTaskControl.ProcessThreadMessages }

Как пишет автор Asy_DispatchMessages - вероятно самая сложная часть OTL и как только Вы начнете  понимать принцип работы, Вы будете полностью подготовлены к написанию локальных петель сообщений в фоновом коде.

3 комментария:

  1. Отлично. Схжие идеи иногда и мне приходили, замечательно, что это объяснено.

    Продолжение будет?

    ОтветитьУдалить
  2. Предположим, что приложение запустилось. На заднем фоне запустился нужный процесс - пусть то же считывание страницы из Интернета. С таймаутом в 30 секунд. Но уже на 2-ой секунде пользователь, раз, и закрывает главное окно программы. Непонятно тогда, как корректно остановить параллельный поток при этом, который выполняется где-то в недрах ICS (или любой другой сторонней библиотеки)?

    ОтветитьУдалить
  3. Так для этого и сделан
    TGpHttpRequest.Cleanup
    когда пользователь завершает программу OTL автоматически завершает работу всех потоков, при завершении вызывается метод Cleanup где и происходит удаление объектов ICS

    ОтветитьУдалить