Робота з потоками в Delphi - Приклади - Delphi - Каталог статей - Вірусологія, погляд з Delphi

Нерідко зустрічав на форумах думки, що потоки не потрібні взагалі, будь-яку програму можна написати так, що вона чудово працюватиме і без них. Звичайно, якщо не робити нічого серйознішого "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;

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 (нагадаю, виконання основного потоку поки що "припинено"), відповідно за цей час встигнутьзмінити будь-які дані. І тут раптом знову віддається основному потоку, він спокійно продовжує своє виконання, але дані вже змінені! До чого це може призвести – передбачити не можна. Ви можете перевірити це тисячу разів, і нічого не станеться, а на тисяча перша програма впаде. І це стосується не тільки взаємодії додаткових потоків з головним, але й взаємодії потоків між собою. Писати такі ненадійні програми, звісно, ​​не можна.

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

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 sleep(50); SendMessage(Form1.Handle,PROGRESS_POS,0,i); end; end;

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

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

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

Працюють вони таким чином: усередині критичної секції може працювати лише один потік, інші чекають на його завершення. Щоб краще зрозуміти, скрізь наводять порівняння з вузькою трубою: уявіть, з одного боку "товпляться" потоки, але в трубу може "пролізти" тільки один, а коли він "пролізе" - почне рух другий, і так по порядку. Ще простіше зрозуміти це на прикладі і тим же ProgressBar"ом. Отже, запустіть один із прикладів, наведених раніше. Натисніть на кнопку, зачекайте кілька секунд, а потім натисніть ще раз. Що відбувається? ProgressBar почав стрибати. Стрибає тому, що у нас працює не один потік, а два, і кожен з них передає різні значення прогресу.Тепер трохи переробимо код, у події на Create форми створимо критичну секцію: 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'ї все досить докладно описано. Мета цієї статті - показати початківцям, що не все так складно і страшно, головне розібратися, що є що. І більше практики - найголовніше досвід!