вторник, 5 октября 2010 г.

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

Практика OTL. Фоновый поиск файлов.

В этом разделе мы немного отвлечемся от исследования возможностей OTL. Перейдем так сказать к практике. И начнем рассмотрение с демонстрационного примера tests\23_BackgroundFileSearch показывающего как с помощью OTL можно сделать фоновый поиск файлов.
Откроем данный пример в IDE.
Как вы видите, у нашего приложения будет простой интерфейс:
В строке ввода мы будем вводить путь и маску поиска, после чего нажав кнопку “Scan” приложение начнет просматривать заданную папку на наличие файлов заданных в маске и в поле “Found” отображать количество найденных файлов в поле.

После того как процесс сканирование закончится в списке появятся найденные файлы.
Во время сканирования, основной (GUI) поток программы  будет доступен, вы спокойно сможете перемещать, сворачивать, максимизировать окно программы.
Стоит также отметить, что помимо компонентов, которые видны на скриншоте, на форме присутствуют также компоненты TOmniEventMonitor (назван OTLMonitor) и TTimer (tmrDisplayStatus).
Когда пользователь щелкает кнопкой Scan, то создается фоновая задача и она запускается на выполнение.
test_23_BackgroundFileSearch.pas   Запуск фоновой задачи
procedure TfrmBackgroundFileSearchDemo.btnScanClick(Sender: TObject);
begin
  FFileList := TStringList.Create;
  btnScan.Enabled := false;
  tmrDisplayStatus.Enabled := true;
  FScanTask := CreateTask(ScanFolders, 'ScanFolders')
    .MonitorWith(OTLMonitor)
    .SetParameter('FolderMask', inpFolderMask.Text)
    .Run;
end;

Рабочий метод фоновой задачи ScanFolders, который выполняется во второстепенном потоке. Мы возвратимся к этому методу позже. Задача будет запущена с мониторингом компонента OTLMonitor. Это сделано так, чтобы мы имели возможность получать сообщения с фоновой задачи. OTLMonitor также проинформирует нас об завершении задачи. Входная папка и маска поиска, посылаются в фоновую задачу как параметр FolderMask. Переменная FFileList типа TStringList  будет содержать список всех найденных файлов.
Когда задача закончит свою работу, то  будет вызван обработчик OTLMonitor.OnTaskTerminated:
test_23_BackgroundFileSearch.pas   Обработчик окончания фоновой (завершения) задачи
procedure TfrmBackgroundFileSearchDemo.OTLMonitorTaskTerminated(
  const task: IOmniTaskControl);
begin
  tmrDisplayStatus.Enabled := false;
  outScanning.Text := '';
  outFiles.Text := IntToStr(FFileList.Count);   // кол-во найденных файлов
  lbFiles.Clear;
  lbFiles.Items.AddStrings(FFileList); // копируем список файлов
  FreeAndNil(FFileList);
  FScanTask := nil;                   // Очищаем задачу
  btnScan.Enabled := true;
end;

Вроде все понятно, но что делать, если пользователь закрывает программу, в то время как второстепенный поток сканирования является активным? В этом случае мы обрабатываем событие OnFormCloseQuery где указываем фоновой задаче закончить свою работу.
test_23_BackgroundFileSearch.pas   При выходе из программы во время активности фоновой задачи
procedure TfrmBackgroundFileSearchDemo.FormCloseQuery(Sender: TObject;
  var CanClose: boolean);
begin
  if assigned(FScanTask) then begin
     FScanTask.Terminate;
     FScanTask := nil;
     CanClose := true;
  end;
end;

Метод Terminate сделает две вещи – он скажет задаче, что нужно завершить работу, затем дождется ее завершения. После этого, мы просто очистим интерфейсную ссылку на задачу. После чего  позволим программе завершится.
Далее мы рассмотрим метод ScanFolders  который является главным методом фоновой задачи,  да да – это тот самый метод, который мы передали в CreateTask.
Метод разбивает значение, переданное в параметре FolderMask на папку и маску поиска, а затем передает эти значение главному рабочему методу ScanFolder.
test_23_BackgroundFileSearch.pas   ScanFolders
procedure ScanFolders(const task: IOmniTask);
var
  folder: string;
  mask  : string;
begin
  mask := task.ParamByName['FolderMask'];
  folder := ExtractFilePath(mask);
  Delete(mask, 1, Length(folder));
  if folder <> '' then
    folder := IncludeTrailingPathDelimiter(folder);
  ScanFolder(task, folder, mask);
end;

ScanFolder сначала находит все подпапки выбранной  папки и вызывает себя рекурсивно для каждой подпапки. Это означает, что сначала обработаются самые глубокие папки и затем алгоритм поиска поднимается к вершине дерева папки. При  нахождении подпапки посылается сообщение  MSG_SCAN_FOLDER в основной GUI-поток. В параметре этого сообщения посылается название обрабатываемой папки.
test_23_BackgroundFileSearch.pas   ScanFolders
const
  MSG_SCAN_FOLDER  = 1;
  MSG_FOLDER_FILES = 2;

procedure ScanFolder(const task: IOmniTask; const folder, mask: string);
var
  err        : integer;
  folderFiles: TStringList;
  S          : TSearchRec;
begin
  err := FindFirst(folder + '*.*', faDirectory, S);
  if err = 0 then
  try
    repeat
      if ((S.Attr and faDirectory) <> 0) and
          (S.Name <> '.') and
          (S.Name <> '..')
       then
          ScanFolder(task, folder + S.Name + '\', mask);   // рекурсия
      err := FindNext(S);
    until task.Terminated or (err <> 0);
  finally
     FindClose(S);
  end;
  task.Comm.Send(MSG_SCAN_FOLDER, folder);
 
  folderFiles := TStringList.Create;
  try
    err := FindFirst(folder + mask, 0, S);
    if err = 0 then try
      repeat
        folderFiles.Add(folder + S.Name);
        err := FindNext(S);
      until task.Terminated or (err <> 0);
    finally
      FindClose(S);
    end;
  finally
    task.Comm.Send(MSG_FOLDER_FILES, folderFiles);
  end;
end;

Метод ScanFolder с помощью файловых функции FindFirst/FindNext/FindClose управляет циклом поиска файлов в папке. Каждый найденный по маске файл будет добавлен к внутреннему объекту TStringList. Когда просмотр папки будет закончен, этот объект посылается в главную нить программы как параметр сообщения MSG_FOLDER_FILES.
Такой подход - посылка данных для одной папки - является компромиссом между возвращением полного набора (полное просмотренное дерево) и возвратом каждого файла в отдельности. В первом случае – это не обеспечивало бы хорошую обратную связь, во втором излишне нагружало бы систему.
Оба  поисковых цикла проверяют статус задачи с помощью функции Terminated и если она возвращает True то цикл будет прерван немедленно. Такой код позволяет нам завершить фоновую задачу, когда пользователь закрывает приложение во время сканирования (при вызове обработчика OnFormCloseQuery).
Это - все, что должно быть сделано в фоновой задаче, но не все, что касается целиком программы. Мы еще должны обработать сообщения в GUI-потоке (основном потоке программы). Для этого мы напишем обработчик OTLMONITOR OnTaskMessage.
test_23_BackgroundFileSearch.pas   Обработчик сообщений в GUI-потоке
procedure TfrmBackgroundFileSearchDemo.OTLMonitorTaskMessage(
  const task: IOmniTaskControl);
var
  folderFiles: TStringList;
  msg        : TOmniMessage;
begin
  task.Comm.Receive(msg);
  if msg.MsgID = MSG_SCAN_FOLDER then
    FWaitingMessage := msg.MsgData
  else
    if msg.MsgID = MSG_FOLDER_FILES then
    begin
       folderFiles := TStringList(msg.MsgData.AsObject);
       FFileList.AddStrings(folderFiles);
       FreeAndNil(folderFiles);
       FWaitingCount := IntToStr(FFileList.Count);
    end;
end;
При получении сообщения MSG_SCAN_FOLDER, мы копируем название папки в локальную  переменную. Если пришло сообщение - MSG_FOLDER_FILES, мы скопируем переданные в параметре сообщения имена найденных файлов (TStringList) в глобальный объект FFileList,. А также обновим число найденных в настоящее время файлов.
Почему же мы  не обновляем сразу список файлов и счетчик на форме при приеме сообщения. В этом нет никакого смысла, так как фоновая задача может послать много сообщений (когда в папке много файлов удовлетворяющих критерию поиска)  в секунду времени и нет никакого смысла обрабатывать их всех по очереди  (постоянно перерисовывая GUI), это замедлило бы работу программы и привело бы к излишней нагрузке на Windows.
Поэтому хорошим тоном является немного другой подход. Вместо постоянного обновления по событию, Мы будем обновлять наши данные по таймеру, который будет вызываться три раза в секунду. Это конечно не позволит показать все просмотренные папки и все промежуточные результаты, но все-же предоставит пользователю приемлемую обратную связь без излишней потери скорости работы.
test_23_BackgroundFileSearch.pas   Обновление данных на форме по таймеру
procedure TfrmBackgroundFileSearchDemo.tmrDisplayStatusTimer(Sender: TObject);
begin
  if FWaitingMessage <> '' then begin
    outScanning.Text := FWaitingMessage;
    FWaitingMessage := '';
  end;
  if FWaitingCount <> '' then begin
    outFiles.Text := FWaitingCount;
    FWaitingCount := '';
  end;
end;
На этом засим отклонятся,  полностью функциональный фоновый сканер файла, готов.

Комментариев нет:

Отправить комментарий