Робота з потоками і логування в Delphi

Автор: © 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, інша пише тільки в балку. Стосується в реальному проекті!

Схожі статті:


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

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*