Робота з потоками і логування в 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>

*

*