Потоки и методы их синхронизаций в Delphi

Статья призвана дать понятия о процессах, потоках и принципах программирования многопоточных приложений в 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 и ожидаемые таймеры.
Теги:
потоки
Добавлено: 04 Августа 2018 09:02:37 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...