Робота з потоками в Delphi: чи такий страшний чорт

Автор: Юрій Баликін, Королівство Delphi


Дана стаття призначена для початківців програмістів, які ніколи не працювали з потоками, і хотіли б дізнатися основи роботи з ними. Бажано, щоб читач знав основи ООП і мав який-небудь досвід роботи в Delphi. Для початку давайте визначимося, що під словом "потік" я маю на увазі саме Thread, який ще має назву "нитка".


Нерідко зустрічав на форумах думки, що потоки не потрібні взагалі, будь-яку програму можна написати так, що вона буде чудово працювати й без них. Звичайно, якщо не робити нічого серйозніше "Hello World" це так і є, але якщо поступово набирати досвід, рано чи пізно будь-який програміст упреться в можливості "плоского" коду, виникне необхідність распараллеліть завдання. А деякі завдання взагалі не можна реалізувати без використання потоків, наприклад робота з сокетами, COM-портом, тривале очікування будь-яких подій, і т.д.


Всім відомо, що Windows система багатозадачна. Простіше кажучи, це означає, що кілька програм можуть працювати одночасно під управлінням ОС. Всі ми відкривали диспетчер завдань і бачили список процесів. Процес – це екземпляр виконуваного додатку. Насправді сам по собі він нічого не виконує, він створюється під час запуску програми, містить у собі службову інформацію, через яку система з ним працює, так само йому виділяється необхідна пам'ять під код і дані. Для того, щоб програма запрацювала, в ньому створюється потік. Будь-який процес містить у собі хоча б один потік, і саме він відповідає за виконання коду і отримує на це процесорний час. Цим і досягається уявна паралельність роботи програм, або, як її ще називають, псевдопаралельною. Чому уявна? Та тому, що реально процесор в кожний момент часу може виконувати тільки одну ділянку коду. Windows роздає процесорний час всіх потоків в системі по черзі, тим самим створюється враження, що вони працюють одночасно. Реально працюють паралельно потоки можуть бути тільки на машинах з двома і більше процесорами.


Для створення додаткових потоків в Delphi існує базовий клас TThread, Від нього ми й будемо успадковуватися при реалізації своїх потоків. Для того, щоб створити "кістяк" нового класу, можна вибрати в меню File – New – Thread Object, Delphi створить новий модуль із заготівлею цього класу. Я ж для наочності опишу його в модулі форми. Як бачите, в цій заготівлі доданий один метод – Execute. Саме його нам і потрібно перевизначити, код всередині нього і буде працювати в окремому потоці. І так, спробуємо написати приклад – запустимо в потоці нескінченний цикл:

TNewThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

{ TNewThread }

procedure TNewThread.Execute;
begin
while true do {нічого не робимо};
end;

procedure TForm1.Button1Click(Sender: TObject);
var
NewThread: TNewThread;
begin
NewThread:=TNewThread.Create(true);
NewThread.FreeOnTerminate:=true;
NewThread.Priority:=tpLower;
NewThread.Resume;
end;


Запустіть приклад на виконання і натисніть кнопку. Начебто нічого не відбувається – форма не зависла, реагує на переміщення. Насправді це не так – відкрийте диспетчер завдань і ви побачите, що процесор завантажений по-повній. Зараз у процесі вашої програми працює два потоки – один був створений спочатку, під час запуску програми. Другий, який так вантажить процесор – ми створили після натискання кнопки. Отже, давайте розберемо, що ж означає код в Button1Click:



NewThread:=TNewThread.Create(true);
тут ми створили екземпляр класу TNewThread. Конструктор Create має всього один параметр – CreateSuspended типу boolean, який вказує, запустити новий потік відразу після створення (якщо false), або дочекатися команди (якщо true).
New.FreeOnTerminate := true;
властивість FreeOnTerminate визначає, що потік після виконання автоматично завершиться, об'єкт буде знищений, і нам не доведеться його знищувати вручну. У нашому прикладі це не має значення, так як сам по собі він ніколи не завершиться, але знадобиться в наступних прикладах.
NewThread.Priority:=tpLower;
Властивість Priority, якщо ви ще не здогадалися з назви, встановлює пріоритет потоку. Так так, кожен потік в системі має свій пріоритет. Якщо процесорного часу не вистачає, система починає розподіляти його відповідно до пріоритетів потоків. Властивість Priority може приймати такі значення:

  • tpTimeCritical – критичний
  • tpHighest – дуже високий
  • tpHigher – високий
  • tpNormal – середній
  • tpLower – низький
  • tpLowest – дуже низький
  • tpIdle – потік працює під час простою системи
Ставити високі пріоритети потоків не варто, якщо цього не вимагає завдання, так як це сильно навантажує систему.
NewThread.Resume;
Ну і власне, запуск потоку.

Думаю, тепер вам зрозуміло, як створюються потоки. Зауважте, нічого складного. Але не все так просто. Здавалося б – пишемо будь-який код усередині методу Execute і все, а ні, потоки мають одна неприємна властивість – Вони нічого не знають один про одного. І що такого? – Запитаєте ви. А ось що: припустимо, ви намагаєтеся з іншого потоку змінити властивість якогось компоненту на формі. Як відомо, VCL однопоточному, весь код всередині програми виконується послідовно. Припустимо, в процесі роботи змінилися якісь дані всередині класів VCL, система відбирає час у основного потоку, передає по колу іншим потокам і повертає назад, при цьому виконання коду продовжується з того місця, де призупинилося. Якщо ми зі свого потоку щось змінюємо, наприклад, на формі, задіюється багато механізмів всередині VCL (нагадаю, виконання основного потоку поки "призупинено"), відповідно за цей час встигнуть змінитися будь-які дані. І тут раптом час знову віддається основному потоку, він спокійно продовжує своє виконання, але дані вже змінені! До чого це може призвести – вгадати не можна. Ви можете перевірити це тисячу разів, і нічого не відбудеться, а на тисячу перший програма звалиться. І це відноситься не тільки до взаємодії додаткових потоків з головним, а й до взаємодії потоків між собою. Писати такі ненадійні програми звичайно не можна.


Ось ми і підійшли до дуже важливого питання – синхронізації потоків.


Якщо ви створили шаблон класу автоматично, то, напевно, помітили коментар, який дружелюбна Delphi помістила в новий модуль. Він говорить: "Methods and properties of objects in visual components can only be used in a method called using Synchronize ". Це означає, що звернення до візуальних компонентів можливо тільки шляхом виклику процедури Synchronize. Давайте розглянемо приклад, але тепер наш потік не буде розігрівати процесор даремно, а буде робити що-небудь корисне, наприклад, прокручувати ProgressBar на формі. Як параметр в процедуру Synchronize передається метод нашого потоку, але сам він передається без параметрів. Параметри можна передати, додавши поля потрібного типу в опис нашого класу. У нас буде одне поле – той самий прогрес:

TNewThread = class(TThread)
private
Progress: integer;
procedure SetProgress;
protected
procedure Execute; override;
end;

procedure TNewThread.Execute;
var
i: integer;
begin
for i:=0 to 100 do
begin
sleep(50);
Progress:=i;
Synchronize(SetProgress);
end;
end;

procedure TNewThread.SetProgress;
begin
Form1.ProgressBar1.Position:=Progress;
end;


Ось тепер ProgressBar рухається, і це цілком безпечно. А безпечно ось чому: процедура Synchronize на час призупиняє виконання нашого потоку, і передає управління не оцінить, тобто SetProgress виконується у головному потоці. Це потрібно запам'ятати, тому що деякі допускають помилки, виконуючи всередині Synchronize тривалу роботу, при цьому, що очевидно, форма зависає на тривалий час. Тому використовуйте Synchronize для виведення інформації – те саме двіганіе прогресу, оновлення заголовків компонентів і т.д.


Ви напевно помітили, що всередині циклу ми використовуємо процедуру Sleep. У однопоточному додатку Sleep використовується рідко, а ось в потоках його використовувати дуже зручно. Приклад – нескінченний цикл, поки не виконається якесь умова. Якщо не вставити туди Sleep ми будемо просто навантажувати систему марною роботою.


Сподіваюся, ви зрозуміли як працює Synchronize. Але є ще один досить зручний спосіб передати інформацію формі – посилка повідомлення. Давайте розглянемо і його. Для цього оголосимо константу:

const
PROGRESS_POS = WM_USER+1;

У оголошення класу форми додамо новий метод, а потім і його реалізацію:

TForm1 = class(TForm)
Button1: TButton;
ProgressBar1: TProgressBar;
procedure Button1Click(Sender: TObject);
private
procedure SetProgressPos (var Msg: TMessage); message PROGRESS_POS;
public
{ Public declarations }
end;

procedure TForm1.SetProgressPos(var Msg: TMessage);
begin
ProgressBar1.Position:=Msg.LParam;
end;


Тепер ми трохи змінимо, можна сказати навіть спростимо, реалізацію методу Execute нашого потоку:

procedure TNewThread.Execute;
var
i: integer;
begin
for i:=0 to 100 do
begin
sleep(50);
SendMessage(Form1.Handle,PROGRESS_POS,0,i);
end;
end;

Використовуючи функцію SendMessage, ми посилаємо вікна додатка повідомлення, один з параметрів якого містить потрібний нам прогрес. Повідомлення стає в чергу, і згідно цієї черги буде оброблено головним потоком, де і виконається метод SetProgressPos. Але тут є один нюанс: SendMessage, як і у випадку з Synchronize, призупинить виконання нашого потоку, поки основний потік не обробить повідомлення. Якщо використовувати PostMessage цього не станеться, наш потік відправить повідомлення і продовжить свою роботу, а вже коли воно там буде опрацьовано – неважливо. Яку з цих функцій використовувати – вирішувати вам, все залежить від завдання.


Ось, в принципі, ми і розглянули основні способи роботи з компонентами VCL з потоків. А як бути, якщо в нашій програмі не один новий потік, а кілька? І треба організувати роботу з одними і тими ж даними? Тут нам на допомогу приходять інші способи синхронізації. Один з них ми і розглянемо. Для його реалізації потрібно додати в проект модуль SyncObjs.


Самий цікавий спосіб, на мій погляд – критичні секції


Працюють вони в такий спосіб: усередині критичної секції може працювати тільки один потік, інші чекають його завершення. Щоб краще зрозуміти, скрізь призводять порівняння з вузькою трубою: уявіть, з одного сторони "товпляться" потоки, але в трубу може "пролізти" тільки один, а коли він "пролізе" – почне рух другий, і так по порядку. Ще простіше зрозуміти це на прикладі і тим же ProgressBar "ом. Отже, запустіть один з прикладів, наведених раніше. Натисніть на кнопку, зачекайте кілька секунд, а потім натисніть ще раз. Що відбувається? ProgressBar почав стрибати. Стрибає тому, що у нас працює не один потік, а два, і кожен з них передає різні значення прогресу. Тепер трохи переробимо код, у події onCreate форми створимо критичну секцію:

var
Form1: TForm1;
CriticalSection: TCriticalSection;

procedure TForm1.FormCreate(Sender: TObject);
begin
CriticalSection:=TCriticalSection.Create;
end;


У TCriticalSection є два потрібні нам методу, Enter і Leave, відповідно вхід і вихід з неї. Помістимо наш код у критичну секцію:

procedure TNewThread.Execute;
var
i: integer;
begin
CriticalSection.Enter;
for i:=0 to 100 do
begin
sleep(50);
SendMessage(Form1.Handle,PROGRESS_POS,0,i);
end;
CriticalSection.Leave;
end;

Спробуйте запустити програму і натиснути кілька разів на кнопку, а потім порахуйте, скільки разів пройде прогрес. Зрозуміло, в чому суть? Перший раз, натискаючи на кнопку, ми створюємо потік, він займає критичну секцію і починає роботу. Натискаємо другий – створюється другий потік, але критична секція зайнята, і він чекає, поки її не звільнить перший. Третій, четвертий – всі пройдуть тільки по-черзі.


Критичні секції зручно використовувати при обробці одних і тих же даних (списків, масивів) різними потоками. Зрозумівши, як вони працюють, ви завжди знайдете їм застосування.


У цій невеликій статті розглянуті не всі способи синхронізації, є ще події (TEvent), а так само об'єкти системи, такі як м'ютекси (Mutex), семафори (Semaphore), але вони більше підходять для взаємодії між додатками. Останнє, що стосується використання класу TThread, ви можете дізнатися самостійно, в help "е все досить докладно описано. Мета цієї статті – показати початківцям, що не все так складно і страшно, головне розібратися, що є що. І побільше практики – найголовніше досвід!

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


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

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

Ваш отзыв

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

*

*