Работа с потоками и логирование в Delphi

Источник: delphikingdom
Erik Ivanov

Автор: © Erik Ivanov

Данная статья - об упрощении работы с потоками и логировании. Как средство, доступное конечному программисту, реализован класс ведения логов. Этот наследник служит для скрытия сложностей работы с потоками, ускоренного написания кода и уменьшения количества багов. Отмечу, что речь идет не о компоненте, это просто библиотека кода.

Описание задачи

Очень грубо можно сказать, что поток представляет из себя цикл, который работает на параллельном процессоре. Многие программисты используют для управления циклом переменые. При чем знают, что существует такой зверь как событие TEvent. И боятся его использовать, так как считают, что переменные для управлением цикла более привычны и знакомы. В данной статье я представляю класс, который снимает большинство проблем и позволяет просто и эффективно применять потоки.

Кроме проблем управления существует проблема ожидания. Если выполняется какой-либо код, то процеcсор вынужден тратить свое время на его выполнение. Поэтому для ожидания была применена специальная API функция MsgWaitForMultipleObjects. В качестве базового класса был выбран TThread, от него был написан наследник TCustomThread, в котором и использованы вышеописанные функции.

Мне необходим был класс, который брал на себя вечно повторяющийся код и при этом позволял гибко задавать необходимое число событий. То есть: если мне необходимы 3 события, то я хотел их использовать, не изменяя исходного кода TCustomThread. Для этого я вставил такую строку {$I ActiveEvent.inc}. Дело в том, что файл ActiveEvent.inc нужно включить в свой проект и именно этот фаил будет использован при компиляции. В нем описаны события как перечисляемый тип.

TActiveEvent = (actExit, actSend, actMonth, actTest);

События actExit, actSend должны существовать всегда. Первый выполняет код, обеспечивающий завершение работы потока, например освобождение ресурсрв(освобождать надо в том же потоке, что и создавали) и сигнализация другому потоку. Второй actSend выполняет основную работу потока. Во многих задачах этих событий достаточно, но если нам нужны еще события, то добавляем. Мне понадобилось событие, срабатывающее раз в месяц, и еще одно раз в день. Событие actTest было предназначено для проверки работоспособности сервиса, который висел годами. Потому что событие actMonth было очень важным и необходимо было выполнить его точно в этот день. Вот краткий пример событий.

Для связи этих событий с процедурами надо в конструкторе написать следуюций код:

tmEvent.Call[actMonth] := MonthEvent;
tmEvent.Call[actTest] := TestEvent;
Где tmEvent: RAction;
RAction = record
  Event: array[TActiveEvent] of THandle;
  Call: array[TActiveEvent] of TNotifyEvent;
end;

TNotifyEvent это стандартное событие в Delphi "procedure (Sender: TObject) of object".

Далее идет описание класса:

TCustomThread = class(TThread)
private
  fTimeOut: Longword;
  fAfterExecute: TNotifyEvent;
  fCall: Integer;
  fsData: TObject;
  function GetID: Integer;
  function GetDescription: String;
  function GetGrupp: String;
  function GetExec: Integer;
protected
  fID: Integer;
  fDescription: String;
  tmEvent: RAction;
  fMetodExec: THandle;
  fLastException: Exception;
  procedure WaitTimeOut; virtual;
  procedure ShowMessage(Value: string); virtual;
  procedure InternalException(const Status: Cardinal); virtual;
  procedure BreakThread(Sender: TObject); virtual;
  procedure InternalExec(Sender: TObject); virtual;
  procedure Execute; override;
  procedure CreateEvent; virtual;
  procedure FreeEvent; virtual;
  procedure DoAfterExecute; virtual;
  procedure SetEvent(Value: TActiveEvent);
  procedure SetTimeOut(Value: Longword);
public
  constructor Create(TimeOut: Longword = INFINITE); virtual;
  destructor Destroy; override;
  procedure Start(const Data: TObject = nil); virtual;
  procedure Stop; virtual;
  procedure WaitExecute;
  function GetTerminated: Boolean; virtual;
  function Active: Boolean;

  property ID: Integer read GetID write fID;
  property Description: String read GetDescription;
  property Exec: Integer read GetExec;
  property OnAfterExecute: TNotifyEvent read fAfterExecute write fAfterExecute;
end;

Назначение методов

Прежде всего Execute. Его нельзя больше перекрывать - это ядро всего класса. В нем используются события, которые мы описали в перечислении TActiveEvent и функция WaitForMultipleObjects. Для пользовательского кода предназначены процедуры InternalExec и те адреса, которые мы присвоили в tmEvent.Call. То есть для обработки события необходимо перекрыть InternalExec, можно не вызывать inherited, но лучше вызвать. Далее можно перекрыть WaitTimeOut. Это событие периодически будет возникать, когда в течении fTimeOut не было ни одного события. По умолчанию устанавливается в INFINITE, то есть в бесконечность и никогда не возникает. Это событие мы будем в дальнейшем использовать для сброса закешированых данных на диск. ShowMessage предназначен для вывода текста прямо из потока. Процедуры CreateEvent; FreeEvent служат для создания и освобождения TEvent. DoAfterExecute срабатывает после выполнения рабочих событий, всех событий за исключением actExit. В InternalException передается обработанная ошибка, можно ее вывести в лог. SetEvent возбуждает указанное событие и соответственно начинает выполнятся код, не надо забывать, что если какое-то событие уже активно и его код выполняется, то новое событие будет выполнено после. Управление возвращается сразу без задержки, как это и полагается для эвентов. Start активизирует событие actSend и запоминает входящие данные. Stop останавливает поток, кроме того ожидает завершения работы потока. В этом методе использована функция MsgWaitFor для корректного неблокирующего ожидания. В оригинале эту предложил использовать MBo, а я сделал ее рабочей для не интерактивных сервисов и обычных приложений. Эта функция позволяет не замораживать визуальные элементы и окна когда ожидается завершение потока. Краткое описание класса закончено, далее я буду расматривать перекрытие методов для наследника TLogThread.

От класса TCustomThread можно получить почти любую функциональность, сделав от него наследника. Таким наследником является TLogThread, который ведет лог программы. Для этого мы перекрываем конструктор:

constructor TLogThread.Create(FileName: String = ''; TimeOut: Longword = 5000);
begin
  inherited Create(TimeOut);
  if FileName = '' then
    fLogName := ChangeFileExt( GetModuleFileName , '.log1');
  Critical := TCriticalSection.Create;
  Buffer := TArr.Create;
  Buffer.MemAloc := False;
end;

TArr это просто небольшой наследник от TList для управления памятью. Главная изюминка состоит в задании TimeOut. Теперь, перекрыв метод WaitTimeOut, мы всегда можем получить управление, если в течении 5с не пришло ни одного события на запись. В нем:

procedure TLogThread.WaitTimeOut;
begin
  SaveAll;
  LogClose;
end;

Сохраняем все на диск и закрываем файл. Таким образом на скорость нашего приложения это лог никак не влияет. А закрытый фаил - это очень важно в сервисах, которые работают 24 часа, у нас всегда есть возможность его стереть и таким образом очистить. Приводить в статье полной код логера не буду, вы можете озакомиться с ним в приложении. Для новичков отмечу, что для обращения к Buffer используются критическая секция Critical. Вроде ничего нестандартного, но очень удобно. Вот описание логера:

TLogThread = class(TCustomThread)
private
  Buffer: TArr;
  Critical: TCriticalSection;
  fFileActive: Boolean;
  fLog: TextFile;
  fLogName: String;
  procedure SetLogName(Value: String);
protected
  procedure WaitTimeOut; override; //virtual;
  procedure BreakThread(Sender: TObject); override;
  procedure InternalExec(Sender: TObject); override;
  procedure InternalException(const Status: Cardinal); override;
  procedure ClealBuf;
  procedure SaveAll;
public
  constructor Create(FileName: String = ''; TimeOut: Longword = 5000); reintroduce;
  destructor Destroy; override;
  procedure Stop; override;
  procedure SaveMem(const Buf: String); virtual;
  procedure SaveFile(const Buf: String);
  procedure LogClose;
  property LogName: String read fLogName write SetLogName;
end;

Для использования лучше всегда использовать SaveMem, но бывают случаи, когда необходимо записать напрямую в фаил, для этого предназначен SaveFile, поэтому он вынесен в Public.

У меня на этом классе реализована еще целая группа наследников, есть TDBThread. Также порождением данной технологии является TReportManager в котором используется TCustomThread. Реализована довольно проблемная вещь, ситема подержки параметров отчетов в базе данных. Причем там есть 3 вида отчетов: просто StoredProc и настоящие визуальные отчеты(QR), кроме того просто одиночные потоки, наследованные от TCustomThread. TReportManager неправильное название, но так сложилось, больше он похож на ThreadManager. Для всех этих вещей в TCustomThread есть поля, которые поначалу кажутся ненужными, например fDescription, но в TReportManager активно используются. Об использовании этих классов будет рассказано в другой статье(если дойдут руки).

Сейчас представляю вашему вниманию пример LogerTest. В нем сделан наследник TLoger от TLogThread. Мне довольно удобно пользоваться такой связкой. В примере реализована передача сообщений от потока в главное приложение. Функции для этого GetMsgStr и SetMsgStr описаны в UtilsLib. Чтобы не менять класс TCustomThread добавил свой стандартный юнит MsgBoxEx.pas, который выводит сообщения без использования VCL и эстонифицирует их(на русский тоже можно перевести заменив константы) через SetWindowsHookEx (WH_CBT. Такой способ замены сообщений в диалогах весьма прост и удобен. Есть две кнопки одна пишет в лог с выдачей из логера в Memo, другая пишет только в лог. Применимо в реальном проекте!


Страница сайта http://185.71.96.61
Оригинал находится по адресу http://185.71.96.61/home.asp?artId=16505