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

SnugForce (Николай Смолин)

Статья призвана дать понятия о процессах, потоках и принципах программирования многопоточных приложений в 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 и ожидаемые таймеры.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=6105