Delphi програмування потоків

При запуску програми система автоматично створює потік для його виконання. Тому якщо додаток однопоточні, то весь код буде виконуватися послідовно, враховуючи всі умовні та безумовні переходи.

Нить забезпечує псевдопараллельную роботу безлічі програм. У деяких випадках без створення потоків не можна обійтися, наприклад, при роботі з сокетами в блокує режимі.

У delphi існує спеціальний клас, який реалізує потоки - tthread. Це базовий клас, від якого треба наслідувати свій клас і перевизначати метод execute.

Тепер можна в тілі процедури tnew.execute писати код, виконання, які підвішують б програму.

Тонкий момент. У тілі процедури не треба викликати метод execute предка.

Тепер необхідно запустити потік. Як всякий клас tnew необхідно створити:

Значення true в методі create значить, що після створення класу потік автоматично запущений не буде.

Потім вказуємо, що після завершення коду потоку він відразу завершиться, тобто не треба піклуватися про його закриття. В іншому випадку, необхідно самим викликати функцію terminate.

Встановлюємо пріоритет в одне з можливих значень:

tpidle Працює, коли система простоює
tplowest найнижчий
tplower Низький
tpnormal Нормальний
tphigher Високий
tphighest Найвищий
tptimecritical Критичний

Не рекомендую встановлювати занадто великий пріоритет тому потік може істотно завантажити систему.

Тонкий момент. Якщо в потоці присутній нескінченний цикл обробки чого-небудь, то потік буде завантажувати систему під зав'язку. Щоб уникнути цього вставляйте функцію sleep (n), де n - кількість мілісекунд, на яке потік призупинить своє виконання, зустрівши це функцію. n слід вибирати в залежності від розв'язуваної задачі.

До речі, якщо Ви плануйте писати код потоку в окремому модулі, то можна трохи спростити написання скелета класу. Для цього виберіть в сховище об'єктів - thread object (Це на закладці new). Вискочить вікно, в якому треба ввести ім'я класу, після чого, натиснувши Ок, автоматично створитися новий модуль зі скелетом Вашого класу.


Синхронізація потоків при зверненні до vcl-компонентів

Значить, ми навчилися створювати потоки. Але тут спливає цікава річ: що буде, якщо два потоки звертаються до одних і тих же даних по запису? Наприклад, два потоки намагаються змінити заголовок головної форми.

Спеціально для цього в ОС реалізовані механізми синхронізацій. Зокрема, в класі tthread є метод дозволяє уникнути паралельного доступу до vcl-компонентів:

procedure synchronize (method: tthreadmethod);

Ось повний приклад, в якому метод addstr додає в memo кілька рядків. Якщо ми просто викличемо метод, то рядки від потоків будуть додадуться в довільному порядку. Якщо addstr викличемо методом synchronize, то рядки додадуться спочатку від одного потоку, а потім від другого. Виходить, що потік монопольно захоплює ресурс memo і додає в нього необхідну інформацію, після додавання потік звільняє memo і ось тепер вже інший потік може додавати в memo свої дані. Поменше слів - більше ресурсів:

uses
windows, messages, sysutils, variants, classes, graphics, controls, form s, dialogs, stdctrls;

procedure tform 1.button1click (sender: tobject);
begin
new1: = tnew.create (true);
new1.freeonterminate: = true;
new1.s: = '1 thread';
new1.priority: = tplowest;
new2: = tnew.create (true);
new2.freeonterminate: = true;
new2.s: = '2 thread';
new2.priority: = tptimecritical;
new1.resume;
new2.resume;
end;

procedure tnew.execute;
begin
synchronize (addstr); // Виклик методу з синхронізацією
// addstr; // Виклик методу без синхронізації
end;

Інші способи синхронізації. модуль syncobjs

У модулі syncobjs знаходяться класи синхронізації, які є обгорткою викликів api-функцій. Всього в цьому модулі оголошено п'ять класів. tcriticalsection, tevent, а так само і простіша реалізація класу tevent - tsimpleevent і використовуються для синхронізації потоків, інші класи можна і не розглядати. Ось ієрархія класів в цьому модулі:

Критичні секції tcriticalsection

Найбільш простим в розумінні є tcriticalsection або критична секція. Код, розташований в критичній секції, може виконуватися тільки одним потоком. В принципі код ніяк не виділяється, а відбувається звернення до коду через критичну секцію. На початку коду знаходиться функція входу в секцію, а по завершенню його вихід з секції. Якщо секція зайнята іншим потоком, то потоки чекають, поки критична секція не звільниться.


На початку роботи критичну секцію необхідно створити:


var
section: tcriticalsection; // глобальна змінна
begin
section.create;
end;

Припустимо, є функція, в якій відбувається додавання елементів в глобальний масив:

Припустимо, цю функцію викликають кілька потоків, тому, щоб не було конфлікту з даними можна використовувати критичну секцію наступним чином:

Уточню, що критичних секцій може бути кілька. Тому при використанні декількох функцій, в яких можуть бути конфлікти за даними треба для кожної функції створювати свою критичну секцію. Після закінчення їх використання, коли функції більше не будуть викликатися, секції необхідно знищити.

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

А ось і приклад, в якому відбувається додавання елемента в динамічний масив. Функція sleep додає затримку в цикл, що дозволяє наочно побачити конфлікт за даними, якщо Ви, звичайно, заберете вхід і вихід з критичної секції в коді.

uses
windows, messages, sysutils, variants, classes, graphics, controls, form s, dialogs, stdctrls, syncobjs;

tnew = class (tthread)
protected
procedure execute; override;
end;

var
form 1: tform 1;
cs: tcriticalsection;
new1, new2: tnew;
mas: array of integer;

procedure tform 1.button1click (sender: tobject);
begin
new1: = tnew.create (true);
new1.freeonterminate: = true;
new1.priority: = tpidle;
new2: = tnew.create (true);
new2.freeonterminate: = true;
new2.priority: = tptimecritical;
new1.resume;
new2.resume;
end;

Для початку не багато про wait-функціях. Це функції, які припиняють виконання потоку. Окремим випадком wait-функції є sleep, як аргумент передається кількість мілісекунд, на яке потрібно заморозити або призупинить потік.

Тонкий момент. Якщо викликати sleep (0), то потік, відмовиться від свого такту - процесорного часу і тут же стане в чергу з готовністю на виконання.

Повної wait-функції в якості параметрів передається дескриптори потоку (ів). Я не буду зупинятися на них зараз докладно. В принципі, wait-функції інкапсулюють деякі класи синхронізації в явному вигляді, решта в НЕ явному вигляді.

Події tevent можуть використовуватися не тільки в багатопотоковому додатку, але і в однопоточном як координації між секціями коду і при передачі даних їх однієї програми до іншої. У багатопотокових застосуваннях використання tevent здається більш розумним і зрозуміліше.

Все відбувається наступним чином. Якщо подія встановлено, то працювати далі можна, якщо подія скинуто, то всі інші потоки чекають. Різниця між подіями і критичними секціями в те, що події перевіряються в коді самого потоку і використовується wait-функція в явному вигляді. Якщо в критичній секції wait-функція виконувалася автоматично, то при використанні подій необхідно викликати її для заморозки потоку.

Події бувають з автоскиданням і без автосброса. З автоскиданням значить, що відразу після повернення з wait-функції подія скидається. При використанні подій без автосброса необхідно самим скидати їх.

Подією без автосброса зручно робити паузу в якомусь певному ділянці коду потоку. Просто зробити паузу в потоці, коли не має значення, де відбудеться заморозка можна використовувати метод tthread.suspend. Події з автоскиданням можна використовувати, так само як і критичні секції.

Для початку подія необхідно створити і бажано до того як будуть створені потоки їх використовують, хоча точніше до виклику wait-функції.

create (eventattributes: psecurityattributes; manualreset, initialstate: boolean; const name: string);

eventattributes - беремо nil.
manualreset - автоскиданням - false, без автосброса - true.
initialstate - початковий стан true - встановлене, false - скинуте.
const name - ім'я події, ставимо порожній. Подія з ім'ям потрібно при обміні даних між процесами.

Все тепер помилки не буде.

Більш простим у використанні є клас tsimpleevent, який є спадкоємцем tevent і відрізняється від нього тільки тим, що його конструктор викликає конструктор предка відразу з встановленими параметрами:

create (nil, true, false, '');

Фактично, tsimpleevent є подія без автосброса, зі скинутим станом і без імені.

Наступний приклад показує, як призупинити виконання потоку в певному місці. В даному прикладі на формі знаходяться три progressbar, потік заповнює progressbar. При бажанні можна призупинити і відновити заповнення progressbar. Як Ви зрозуміли ми будемо створювати подія без автосброса. Хоча тут доречніше використовувати tsimpleevent, ми використовували tevent, тому що освоївши роботу з tevent буде просто перейти на tsimpleevent.

uses
windows, messages, sysutils, variants, classes, graphics, controls, form s, dialogs, stdctrls, syncobjs, comctrls;

type
tform 1 = class (tform)
button1: tbutton;
progressbar1: tprogressbar;
progressbar2: tprogressbar;
progressbar3: tprogressbar;
button2: tbutton;
procedure form create (sender: tobject);
procedure form destroy (sender: tobject);
procedure button1click (sender: tobject);
procedure button2click (sender: tobject);
private

public

end;

tnew = class (tthread)
protected
procedure execute; override;
end;

procedure tform 1.form create (sender: tobject);
begin
// Створюємо подія до того як будемо його використовувати
event: = tevent.create (nil, true, true, '');
// Запускаємо потік
new: = tnew.create (true);
new.freeonterminate: = true;
new.priority: = tplowest;
new.resume;
end;

procedure tform 1.button1click (sender: tobject);
begin
// Встановлюємо подія
// wait-функція буде фозвращать управління відразу
event.setevent;
end;

procedure tform 1.button2click (sender: tobject);
begin
// wait-функція блокує виконання коду потоку
event.resetevent;
end;


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

uses
windows, messages, sysutils, variants, classes, graphics, controls, form s, dialogs, stdctrls, syncobjs, comctrls;

tproc = class (tthread)
protected
procedure execute; override;
end;

tsend = class (tthread)
protected
procedure execute; override;
end;

procedure tform 1.form create (sender: tobject);
begin
// Створюємо подія до того як будемо його використовувати
event: = tevent.create (nil, false, true, '');
// Запускаємо потоки
proc: = tproc.create (true);
proc.freeonterminate: = true;
proc.priority: = tplowest;
proc.resume;
send: = tsend. create (true);
send. freeonterminate: = true;
send. priority: = tplowest;
send. resume;
end;

Ось і всі об'єкти синхронізації модуля syncobjs, яких в принципі вистачить для вирішення різних завдань. У windows існують інші об'єкти синхронізації, які теж можна використовувати в delphi, але вже на рівні api. Це м'ютекси - mutex, семафори - semaphore і очікувані таймери.

Схожі статті