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