Потоки і методи їх синхронізацій в Delphi, HTML, XML, DHTML, Інтернет-технології, статті

Стаття покликана дати поняття про процеси, потоках і принципах програмування багатопоточних додатків в delphi.

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


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


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


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


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


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


tnew = class(tthread)
private
{ private declarations }
protected
procedure execute; override;
end;

procedure tnew.execute;
begin
{ place thread code here }
/ / Код, який буде виконуватися в окремому потоці
end;


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


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


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


var
new: tnew;

begin
new := tnew.create(true);
end;


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


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


new.freeonterminate := true;


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


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


new.priority := tplowest;


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


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


Запускаємо потік:


new.resume;


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


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


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


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


procedure synchronize(method: tthreadmethod);


Він то і дозволяє уникнути конфлікту при зверненні до одних vcl-компонентів різними потоками. Як параметр йому передається адреса процедури без параметрів. А як викликати з параметрами? Для цього можна використовувати внутріклассовие змінні.


tnew = class(tthread)
private
{ private declarations }
st: string;
procedure update;
protected
procedure execute; override;
end;


var
new: tnew;

procedure update;
begin
form1.caption := s;
end;

begin
s := “yes”;
synchronize(update);
end;


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

unit unit1;


interface


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


type
tform1 = class(tform)
memo1: tmemo;
button1: tbutton;
procedure button1click(sender: tobject);
private
{ private declarations }
public
{ public declarations }
end;


tnew = class(tthread)
private
s: string;
procedure addstr;
protected
procedure execute; override;
end;


var
form1: tform1;
new1, new2: tnew;


implementation


{$r *.dfm}


procedure tform1.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;


{ tnew }
procedure tnew.addstr;
begin
form1.memo1.lines.add(s);
sleep(2);
form1.memo1.lines.add(s);
sleep(2);
form1.memo1.lines.add(s);
sleep(2);
form1.memo1.lines.add(s);
sleep(2);
form1.memo1.lines.add(s);
end;


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


end.

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


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



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


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


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



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


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


function addelem(i: integer);
var
n: integer;
begin
n := length(mas);
setlength(mas,n + 1);
mas[n + 1] := i;
end;


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


function addelem(i: integer);
var
n: integer;
begin
section.enter;
n := length(mas);
setlength(mas,n + 1);
mas[n + 1] := i;
section.leave;
end;


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


section.free;


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


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


unit unit1;


interface


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


type
tform1 = class(tform)
button1: tbutton;
memo1: tmemo;
procedure formcreate(sender: tobject);
procedure formdestroy(sender: tobject);
procedure button1click(sender: tobject);
private
{ private declarations }
public
{ public declarations }
end;


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


var
form1: tform1;
cs: tcriticalsection;
new1, new2: tnew;
mas: array of integer;


implementation


{$r *.dfm}


procedure tform1.formcreate(sender: tobject);
begin
setlength(mas,1);
mas[0] := 6;
/ / Створюємо критичну секцію
cs := tcriticalsection.create;
end;


procedure tform1.formdestroy(sender: tobject);
begin
/ / Видаляємо критичну секцію
cs.free;
end;


{ tnew }
procedure tnew.execute;
var
i: integer;
n: integer;
begin
for i := 1 to 10 do
begin
/ / Вхід в критичну секцію
cs.enter;
/ / Код, виконання якого паралельно заборонено
n := length(mas);
form1.memo1.lines.add(inttostr(mas[n-1]));
sleep(5);
setlength(mas,n+1);
mas[n] := mas[n-1]+1;
/ / Вихід з критичної секції
cs.leave;
end;
end;


procedure tform1.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;


end.


Трохи wait-функціях


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


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


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

Події tevent


Події 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 – ім’я події, ставимо пусте. Подія з ім’ям потрібно при обміні даних між процесами.

var
event: tevent;
new1, new2: tnew; / / потоки

begin
event := tevent.create(nil, false, false, “”);
end;
procedure tnew.execute;
var
n: integer;
begin
event.waitfor(infinite);
n := length(mas);
setlength(mas,n + 1);
mas[n + 1] := i;
event.setevent;
end;


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


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


create(nil, true, false, “”);


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


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


unit unit1;


interface


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


type
tform1 = class(tform)
button1: tbutton;
progressbar1: tprogressbar;
progressbar2: tprogressbar;
progressbar3: tprogressbar;
button2: tbutton;
procedure formcreate(sender: tobject);
procedure formdestroy(sender: tobject);
procedure button1click(sender: tobject);
procedure button2click(sender: tobject);
private
{ private declarations }
public
{ public declarations }
end;


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


var
form1: tform1;
new: tnew;
event: tevent;


implementation


{$r *.dfm}


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


procedure tform1.formdestroy(sender: tobject);
begin
/ / Видаляємо подія
event.free;
end;


{ tnew }
procedure tnew.execute;
var
n: integer;
begin
n := 0;
while true do
begin
/ / Wait-функція
event.waitfor(infinite);
if n > 99 then
n := 0;
/ / Одночасно прирощує
form1.progressbar1.position := n;
form1.progressbar2.position := n;
form1.progressbar3.position := n;
/ / Затримка для видимості
sleep(100);
inc(n)
end;
end;


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


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


end.


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


unit unit1;


interface


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


type
tform1 = class(tform)
label1: tlabel;
procedure formcreate(sender: tobject);
procedure formdestroy(sender: tobject);
private
{ private declarations }
public
{ public declarations }
end;


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


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


var
form1: tform1;
proc: tproc;
send: tsend;
event: tevent;


implementation


{$r *.dfm}


procedure tform1.formcreate(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;


procedure tform1.formdestroy(sender: tobject);
begin
/ / Видаляємо подія
event.free;
end;


{ tnew }
procedure tproc.execute;
begin
while true do
begin
/ / Wait-функція
event.waitfor(infinite);
form1.label1.caption := “proccessing…”;
sleep(2000);
/ / Підготовка даних
//…
/ / Дозволяємо працювати іншому потоку
event.setevent;
end;
end;


{ tsend }
procedure tsend.execute;
begin
while true do
begin
/ / Wait-функція
event.waitfor(infinite);
form1.label1.caption := “sending…”;
sleep(2000);
/ / Відсилання даних
//…
/ / Дозволяємо працювати іншому потоку
event.setevent;
end;
end;


end.

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

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


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

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

Ваш отзыв

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

*

*