PDA

Просмотр полной версии : Реализация многопоточности [delphi]


Tip.the.besT
30.03.2012, 16:34
Реализация многопоточности в delphi.​Вижу множество вопросов на разных форумах по программированию которые относятся к многопоточности. В этой статье я бы хотел не только объяснить как реализовать это в delphi, но и предоставить готовый шаблон многопоточного приложения который вы бы могли использовать в дальнейшем в своих программах.

Для начала немного теории. Одной из важнейших вещей в многопоточном приложении является синхронизация потоков. Если не учесть, синхронизацию потоков, то это, может привести к плачевным последствиям. Давайте рассмотрим такой пример, после чего у вас в голове всё встанет на свои места.

У нас в программе имеется переменная, назовём её - "int" - типа integer. Если мы будем присваивать ей значение из основного потока, то всё будет хорошо, и проблем не возникнет. Мало того, если даже мы присвоим ей значение из дополнительного потока который мы создали, то всё будет хорошо. Но так как зачастую у многопоточного приложения более одного дополнительного потока, то зададимся вопросом- "А, что будет если мы попытаемся присвоить переменной значение из двух потоков одновременно?". Вот тут, и произойдёт то таинственное события при котором переменной присвоится значение которое скорее всего не будет равно ни одному из значений которых вы планировали ей присвоить. Тогда и вступает в игру метод синхронизации synchronize. С помощь данного нам разработчиками delphi метода, вы можете обеспечить безопасное обращение нескольких потоков к одной переменной. Данный метод обеспечивает следующие: при вызове процедуры с помощью synchronize она начинает выполнятся, а все остальные процедуры вызванные с помощью этого метода встают в очередь и ждут выполнения текущей процедуры. Всё вышеописанное относится только к записи переменных, а читать можно одновременно из нескольких потоков и ничего плохого не произойдёт. Ну а теперь перейдём к практике.

И так создадим новый проект. И начнём писать поток. После вот этих строк:


private
{ Private declarations }
public
{ Public declarations }
end;

Добавим свои:


potok = class(TThread) //Этой строкой мы унаследовали класс потока
private
str: string;//в разделе private описываются переменные с помощью которых мы
nomer : Integer;//будем передавать значения между процедурами внутри потока
protected
procedure Execute; override;//это главная процедура потока, она начинает свою работу
//после того как мы создали поток
public
procedure synchro;//в разделе public вы можете объявить процедуры какие только душе
//угодно
constructor Create(CreateSuspended: Boolean);//эта строка говорит о том, что мы в
//implementation опишем конструкцию
//потока
end;

Ну здесь вроде бы всё понятно, вы можете добавлять сколько угодно переменных и процедур для удобства работы.

Под implementation нам надо описать конструкцию потока, в данном примере это не обязательно, но, что бы вы знали на будущие мы всё равно это сделаем.


constructor potok.Create(CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);//Эта строка говорит о том, что поток после создания
//будет приостановлен если ему передать значение true при создание, если false, то сразу
//начнёт работу.
end;

Добавим в глобальные переменные переменную - "nom" - типа integer, а так же массив наших потоков.


var
Form1: TForm1;
nom:integer;
a: array [1..10] of potok;//массив для хранения наших потоков

Теперь нужно описать процедуры потока, под implementation добавляем следующие:


procedure potok.Execute;//начинаем описывать главную процедуру потока
var
i:integer;
begin
for i:=0 to 100 do
begin
sleep(1000);
synchronize(synchro);//этой строкой мы вызываем процедуру synchro в единичном экземпляре
end;
end;

procedure potok.synchro; //описываем ещё одну процедуру потока, которая будет менять
//загаловок form1
begin
inc(nom);
form1.Caption:='Поток делает своё дело - '+inttostr(nom);
end;

Собственно всё готово, остаются только запустить потоки. Кидаем кнопку на форму, "кастуем на неё даблклик" и пишем:


procedure TForm1.Button1Click(Sender: TObject);
var
pot:integer;
begin
for pot:=1 to 10 do //цикл запускает 10 потоков, которые будут изменять заголовок
a[pot]:=potok.Create(false); //формы, так же идёт
//добавление в массив, что бы потом вы могли уничтожить
//один поток.
end;

Вот так выглядит программа в итоге:


unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;

procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
potok = class(TThread) //Этой строкой мы унаследовали класс потока
private
str: string;//в разделе private описываются переменные с помощью которых мы
nomer : Integer;//будем передавать значения между процедурами внутри потока
protected
procedure Execute; override;//это главная процедура потока, она начинает свою работу
//после того как мы создали поток
public
procedure synchro;//в разделе public вы можете объявить процедуры какие только душе
//угодно
constructor Create(CreateSuspended: Boolean);//эта строка говорит о том, что мы в
//implementation опишем конструкцию
//потока
end;

var
a: array [1..10] of potok;
Form1: TForm1;
nom:integer;
implementation
constructor potok.Create(CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);//Эта строка говорит о том, что поток после создания
//будет приостановлен если ему передать значение true при создание, если false, то сразу
//начнёт работу.
end;
{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
pot:integer;
begin
for pot:=1 to 10 do //цикл запускает 10 потоков, которые будут изменять заголовок
a[pot]:=potok.Create(false); //формы, так же идёт добавление в массив, что бы потом вы могли их уничтожить по одному.
end;

procedure potok.Execute;//начинаем описывать главную процедуру потока
var
I:integer;
begin
for i:=0 to 100 do
begin
sleep(1000);
synchronize(synchro);//этой строкой мы вызываем процедуру synchro в единичном экземпляре
end;
end;

procedure potok.synchro; //описываем ещё одну процедуру потока, которая будет менять
//загаловок form1
begin
inc(nom);
form1.Caption:='Поток делает своё дело - '+inttostr(nom);
end;

end.

Запускаем программу, и нажимаем на кнопку. И видим как в заголовке формы по очереди увеличивается число. ЧТД как говорится. Удачи в создание приложений.

С уважением Tip.the.besT.

Автор: Tip.the.besT​

mironich
30.03.2012, 16:45
Так запускать поток


potok.Create(false);


Не, ок а если я захочу конкретный экземпляр потока прибить и. т. д. как мне до него добраца.

Synhronize гдето писали что лучше его не использовать для работы с глоб. переменными а только для синхронизации работы с формой.

Описывать класс потока в одном юните с формой не ок.

Шаблон этот создаетcя File->New->Other->ThreadObject

Коментировать так детально код, не ок, все черты мешаються с кодом.


//начинаем описывать главную процедуру потока

Tip.the.besT
31.03.2012, 06:53
Так запускать поток
Не, ок а если я захочу конкретный экземпляр потока прибить и. т. д. как мне до него добраца.
Synhronize гдето писали что лучше его не использовать для работы с глоб. переменными а только для синхронизации работы с формой.
Описывать класс потока в одном юните с формой не ок.
Шаблон этот создаетcя File->New->Other->ThreadObject
Коментировать так детально код, не ок, все черты мешаються с кодом.


Ну с тем, что я не добавил потоки в массив я согласен, статью дополнил.

Не знаю где писали про Synhronize, но если ты запустишь мой пример, то убедишься, что нормально всё с переменными.

Интересно и как же описания потока в одном юните повлияет на работу программы?

Конечно, тебе комментарии не нужны. А человеку придётся возвращаться выше и перечитывать, что бы понять.

Итого. Достаточно было написать: "Добавь потоки в массив."

M_script
31.03.2012, 07:06
Интересно и как же описания потока в одном юните повлияет на работу программы?


Пиши весь код в одну строку, это тоже не повлияет на работу программы.

mironich
31.03.2012, 07:30
Конечно, тебе комментарии не нужны. А человеку придётся возвращаться выше и перечитывать, что бы понять.


НУ я имел введу коментари,


procedure potok.Execute;//начинаем описывать главную процедуру потока


Достаточно прокомментировать в объявлении класса.


synchronize(synchro);//этой строкой мы вызываем процедуру synchro в единичном экземпляре


Подобный комментарий меня вводит в ступор, человека знающего азы делфи, что же будет с новичком?

Зачем переопределять конструктор??

Если в нем нечего не происходит?


procedure synchro;//в разделе public вы можете объявить процедуры какие только душе
//угодно


А в private секции нельзя?

А нужно в приват, это правило хорошего тона что не юзаеться напрямую извне совать в привате.

И одна из таких кому то мб и не покажеться проблемой.

Когда мы объявляем данные в Private секции они доступны классам и т д,

объявленным в том же юните, соответственно безопасность работы с классом нарушается, ну и автоподстройка будет выдавать приват члены класса, а на сонную голову из-за этого можно потом долго ошибку ловить.

Ну и одно из важнейших замечаний при работе с потоками ты не указал надо обрабатывать все Except-шены, иначе поток навернется и память потечет рекой....

Label тоже его может вызвать.,.

Массив для работы с потоками тоже не лучший вариант я об об этом узнал правда только сегодня днем..

TObjectList ок вариант, точнее наследник от него.

Длинные/много строчные комментарии лучше писать так.

{Комент....

Комент,..}

Ну и ты не написал о том что это абстрактный класс и от него всегда надо наследоваца.


Интересно и как же описания потока в одном юните повлияет на работу программы?


Об этом я написал выше, но в сокращенном виде звучит так, классы из того юнита в котором объявлен твой поток будут иметь доступ к Private членам класса.

А память то не освобождаеца....

Это самая большая ошибка, неофиты врятли ее увидят.

И будут думать после нных запусков куда делась память???

M_script
31.03.2012, 07:42
классы из того юнита в котором объявлен твой поток будут иметь доступ к Private членам класса


уверен?

"Директива Private начинает раздел данных (полей) и подпрограмм (методы) класса, которые являются частными (внутренними) для этого класса."

Tip.the.besT
31.03.2012, 07:48
НУ я имел введу коментари,
Достаточно прокомментировать в объявлении класса.
Подобный комментарий меня вводит в ступор, человека знающего азы делфи, что же будет с новичком?
Зачем переопределять конструктор??
Если в нем нечего не происходит?
А в private секции нельзя?
А нужно в приват, это правило хорошего тона что не юзаеться напрямую извне совать в привате.
И одна из таких кому то мб и не покажеться проблемой.
Когда мы объявляем данные в Private секции они доступны классам и т д,
объявленным в том же юните, соответственно безопасность работы с классом нарушается, ну и автоподстройка будет выдавать приват члены класса, а на сонную голову из-за этого можно потом долго ошибку ловить.
Ну и одно из важнейших замечаний при работе с потоками ты не указал надо обрабатывать все Except-шены, иначе поток навернется и память потечет рекой....
Label тоже его может вызвать.,.
Массив для работы с потоками тоже не лучший вариант я об об этом узнал правда только сегодня днем..
TObjectList ок вариант, точнее наследник от него.
Длинные/много строчные комментарии лучше писать так.
{Комент....
Комент,..}
Ну и ты не написал о том что это абстрактный класс и от него всегда надо наследоваца.
Об этом я написал выше, но в сокращенном виде звучит так, классы из того юнита в котором объявлен твой поток будут иметь доступ к Private членам класса.
А память то не освобождаеца....
Это самая большая ошибка, неофиты врятли ее увидят.
И будут думать после нных запусков куда делась память???


Если бы ты читал не поверхностно, то возможно заметил бы следующие:


Под implementation нам надо описать конструкцию потока, в данном примере это не обязательно, но, что бы вы знали на будущие мы всё равно это сделаем.


Каждый решает для себя как оформлять код, и честно говоря я бы лучше послушал комментарии тех людей которые разбираются с потоками по этой статье, и они бы сказали, что им конкретно не понятно.

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

Исключения я с удовольствием добавлю. Так же считаю, что пора завязывать с этим спором, так как каждый будет стоять на своём.

mironich
31.03.2012, 08:05
уверен?


Немного ошибся из других классов доступ не получить(напрямую к полю), а вот у создананого экземпляра можно.


SimpleClass = class
private G: string;
end;



var
Form1: TForm1;
H : SimpleClass;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
H.G := 'g';
end;

Тесть получаем доступ к приват члену в обход свойств(если бы были).

Если все это объявлено в одном юните.

Unknown
31.03.2012, 09:10
а не проще & лучше вместо TThread юзать апи потоки? (beginthread - createthread)

mironich
31.03.2012, 09:17
а не проще вместо TThread юзать апи потоки? (beginthread - createthread)


Не проще особенно когда, ценишь время, тут в два клика создается класс почти готовый к работе, в который можно передавать данные без напрягов, не через глобальные переменные.

С BeginThread для прямой передачи данных надо описывать свой тип передавать в него данные...

Я пользовался сначала BeginThread когда попробовал TThread(научился с ним работать), до этого я не вьежал как переопределить метод и многово не понимал...

Я потом окуел сколько я тратил времени на создание потоков через BeginThread...(В плане велосипедов для передачи данных в поток )

Сча вот пишу компонент чтоб кинул на форму, указатели на форму и нужные классы дал и готово..

И самое главное использование TThread это ООП стиль программирования(если вся программа в нем написана), а BeginThread\CreatThread Функциональный\процедурны (я различие обоих стилей не знаю(функц.проц.)).

Ну и это опять же увеличение повторного использования кода.

Вместо BeginThread(Параметры);

Я зделаю, Thread := TThread.Create(замороженный\не замороженный);

Конечно можно выполнить создание через макросы, но как по мне целесообразней юзать TThread для десктоп софта, не для ботов и.т.д.

M_script
31.03.2012, 09:39
а не проще & лучше вместо TThread юзать апи потоки? (beginthread - createthread)


Чем чистый апи может быть проще или лучше обертки?

ChymeNik
31.03.2012, 14:19
По мне гораздо легче использовать BeginThread/EndThreadEnterCrititcalSection/LeaveCriticalSection


procedure ProcedureForAllThreads();
begin
EnterCriticalSection(CS);
//Тут располагаем код, который должен выполняться только в одном потоке
LeaveCriticalSection(CS);
end;
thread:=BeginThread(nil,1024,@ProcedureForAllThrea ds,nil,0,ThreadIdVarCardinalType);
TerminateThread(thread,0);

mironich
31.03.2012, 14:25
По мне гораздо легче использовать BeginThread/EndThread/EnterCrititcalSection/LeaveCriticalSection!
Зачем такие заморочки с этими тяжелыми обертками?


Чем они тяжелы тем что не заставляют каждый раз одно и тоже писать??

Ну и когда захотеться оптимизировать свою работы, создать класс для работы с потоками под. специфические задачи легче унаследоваца от TThread чем пелить велосипед и отлаживать его.

TThread это прежде всего абстракция над апи потоками дабы программист думал как реализовать функции которые будут в потоке а не как их запилить.

Ну и разработка с TThread быстрее осуществляются и по качеству апи потокам не уступает.

К томуже кучу классов готовых придумали дабы сэкономить наше время.

А где CloseHandle(thread)?

Уу все утечка хэндлов....

На мдсне не нашел строчек о том что TerminateThread хэндл закрывает..


procedure ProcedureForAllThreads();
begin
EnterCriticalSection(CS);
//Тут располагаем код, который должен выполняться только в одном потоке
LeaveCriticalSection(CS);
end;

А убивать через TerminateThread нужно только в крайних случаях, не советуйте плохого)

Лучше через ExitThread();

ChymeNik
31.03.2012, 15:27
А чего именно плохого в закрытии потока через TerminateThread?

Код показан только для примера

mironich
31.03.2012, 15:33
А чего именно плохого в закрытии потока через TerminateThread?


TerminateThread is a
dangerous
function that should only be used in the most extreme cases. You should call TerminateThread only if you know exactly what the target thread is doing, and you control all of the code that the target thread could possibly be running at the time of the termination. For example, TerminateThread can result in the following problems:
If the target thread owns a critical section, the critical section will not be released.
If the target thread is allocating memory from the heap, the heap lock will not be released.
If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread's process could be inconsistent.
If the target thread is manipulating the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL.


(c)mdsn

И чем вообще плохо на апи потоки юзать, нету исключений, проверять рез. выполнения каждой функции ка кто напряжено.

M_script
31.03.2012, 15:34
Лучше через ExitThread();


"Поток можно завершить принудительно, вызвав ExitThread. При этом освобождаются все ресурсы операционной системы, выделенные дан ному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются. Именно поэтому лучше возвращать управление из функции потока, чем самому вызывать функцию ExitThread." (Д. Рихтер)

mironich
31.03.2012, 15:38
"Поток можно завершить принудительно, вызвав ExitThread. При этом освобождаются все ресурсы операционной системы, выделенные дан ному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются. Именно поэтому лучше возвращать управление из функции потока, чем самому вызывать функцию ExitThread." (Д. Рихтер)


Чет я не вкурил.

Но у меня обычно на BeginThread код так.


procedure;
begin

ExitThread(0);
end;



pеcypcы (например, объекты, созданные из С++-классов) не очищаются


В каком понятии очистить если я принудительно вызываю перед завершением потока деструкторы?

M_script
31.03.2012, 15:50
Забыл совсем, что речь о делфи. Не пишу, поэтому не знаю, может там как-то по-другому.

shadowrun
31.03.2012, 17:09
Нового не нашел ничего, при чтении/изучении кода вероятность запутаться больше, нежели что-то понять, особенно для новичков. Был бы благодарен, за четкую, статейку с подробным описание и примерами по синхронизации потоков.

За старания +.

guest11
23.12.2019, 13:55
работает , даже в memo записывает ,

.SpoilerTarget" type="button">Spoiler: 1
unit Unit1;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls;

type

TForm1 = class(TForm)

Button1: TButton;

Memo1: TMemo;

procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

potok = class(TThread) //Этой строкой мы унаследовали класс потока

private

str: string;//в разделе private описываются переменные с помощью которых мы

nomer : Integer;//будем передавать значения между процедурами внутри потока

protected

procedure Execute; override;//это главная процедура потока, она начинает свою работу

//после того как мы создали поток

public

procedure synchro;//в разделе public вы можете объявить процедуры какие только душе

//угодно

constructor Create(CreateSuspended: Boolean);//эта строка говорит о том, что мы в

//implementation опишем конструкцию

//потока

end;

var

a: array [1..10] of potok;

Form1: TForm1;

nom:integer;

implementation

constructor potok.Create(CreateSuspended: Boolean);

begin

inherited Create(CreateSuspended);//Эта строка говорит о том, что поток после создания

//будет приостановлен если ему передать значение true при создание, если false, то сразу

//начнёт работу.

end;

{$R *.dfm}

procedure Ping(IP: String; OutMemo:TMemo);

const BUFSIZE = 2000;

var SecAttr : TSecurityAttributes;

hReadPipe,

hWritePipe : THandle;

StartupInfo: TStartUpInfo;

ProcessInfo: TProcessInformation;

Buffer : Pchar;

WaitReason,

BytesRead : DWord;

begin

with SecAttr do

begin

nlength := SizeOf(TSecurityAttributes);

binherithandle := true;

lpsecuritydescriptor := nil;

end;

if Createpipe (hReadPipe, hWritePipe, @SecAttr, 0) then

begin

Buffer := AllocMem(BUFSIZE + 1);

FillChar(StartupInfo, Sizeof(StartupInfo), #0);

StartupInfo.cb := SizeOf(StartupInfo);

StartupInfo.hStdOutput := hWritePipe;

StartupInfo.hStdInput := hReadPipe;

StartupInfo.dwFlags := STARTF_USESTDHANDLES +

STARTF_USESHOWWINDOW;

StartupInfo.wShowWindow := SW_HIDE;

if CreateProcess(nil,

PChar('ping.exe '+IP),

@SecAttr,

@SecAttr,

true,

NORMAL_PRIORITY_CLASS,

nil,

nil,

StartupInfo,

ProcessInfo) then

begin

repeat

WaitReason := WaitForSingleObject( ProcessInfo.hProcess,100);

Application.ProcessMessages;

until (WaitReason <> WAIT_TIMEOUT);

Repeat

BytesRead := 0;

ReadFile(hReadPipe, Buffer[0], BUFSIZE, BytesRead, nil);

Buffer[BytesRead]:= #0;

OemToAnsi(Buffer,Buffer);

OutMemo.Text := OutMemo.text + String(Buffer);

until (BytesRead