Работа с нишки в Delphi - Примери - Delphi - Каталог със статии - Вирусология, изглед от Delphi
Често срещах мнения във форуми, че нишките изобщо не са необходими, всяка програма може да бъде написана по такъв начин, че да работи чудесно и без тях. Разбира се, ако не правите нищо по-сериозно от „Hello World“, това е вярно, но ако постепенно натрупате опит, рано или късно всеки начинаещ програмист ще се натъкне на възможността за „плосък“ код, ще има нужда от паралелизиране на задачи. И някои задачи изобщо не могат да бъдат изпълнени без използването на нишки, например работа със сокети, COM порт, чакане на всякакви събития за дълго време и т.н.
Всеки знае, че Windows е многозадачна система. Просто казано, това означава, че няколко програми могат да работят едновременно под операционната система. Всички отворихме диспечера на задачите и видяхме списък с процеси. Процесът е екземпляр на работещо приложение. Всъщност той не прави нищо сам по себе си, създава се при стартиране на приложението, съдържа служебна информация, чрез която системата работи с него и му се отделя необходимата памет за код и данни. За да работи програмата, в нея се създава нишка. Всеки процес съдържа поне една нишка и той е този, който отговаря за изпълнението на кода и получава процесорно време за това. Така се постига въображаемият паралелизъм на програмите или, както още го наричат, псевдопаралелизъм. Защо въображаеми? Да, защото в действителност процесорът може да изпълни само един код наведнъж. Windows разпределя процесорното време на всички нишки в системата на свой ред, като по този начин създава впечатлението, че работят едновременно. Нишки, които действително работят паралелно, могат да съществуват само на машини с два или повече процесора.
За да създадете допълнителни нишки в Delphi, има базов клас TThread, ние ще наследим от негопри реализирането на техните потоци. За да създадете "скелет" на нов клас, можете да изберете File - New - Thread Object от менюто, Delphi ще създаде нов модул със заготовка от този клас. За по-голяма яснота ще го опиша в модула формуляр. Както можете да видите, към тази заготовка е добавен един метод - Изпълнение. Ние сме тези, които трябва да го предефинираме, кодът вътре в него ще работи в отделна нишка. И така, нека се опитаме да напишем пример - нека стартираме безкраен цикъл в нишката: TNewThread = class(TThread) private < Лични декларации > защитена процедура Изпълнение; отмяна; край;
var Формуляр1: TForm1;
процедура TNewThread.Execute; започнете докато е истина до ; край;
procedure TForm1.Button1Click(Sender: TObject); var NewThread: TNewThread; begin NewThread:=TNewThread.Create(true); NewThread.FreeOnTerminate:=true; NewThread.Priority:=tpLower; Нова тема.Резюме; край;
Стартирайте примера и щракнете върху бутона. Изглежда, че нищо не се случва - формата не виси, реагира на движенията. Всъщност това не е така - отворете диспечера на задачите и ще видите, че процесорът е напълно зареден. В момента има две нишки, изпълнявани в процеса на вашето приложение - едната е създадена първоначално при стартирането на приложението. Вторият, който толкова натоварва процесора - създадохме с едно натискане на бутон. Така че нека да видим какво означава кодът в Button1Click: NewThread:=TNewThread.Create(true); тук създадохме екземпляр на класа TNewThread. Конструкторът Create има само един параметър - CreateSuspended от булев тип, който указва дали да стартира нова нишка веднага след създаването (ако е false), или да изчака команда (ако е true). New.FreeOnTerminate := true; Свойството FreeOnTerminate определя, че нишката ще прекрати автоматично след изпълнение, обектът ще бъдеунищожени и не трябва да го унищожаваме ръчно. В нашия пример това няма значение, тъй като никога няма да завърши самостоятелно, но ще бъде необходимо в следващите примери. NewThread.Priority:=tpLower; Свойството Priority, ако не сте познали от името, задава приоритета на нишката. Да, да, всяка нишка в системата има свой собствен приоритет. Ако няма достатъчно процесорно време, системата започва да го разпределя според приоритетите на нишките. Свойството Priority може да приеме следните стойности: tpTimeCritical - критичен tpHighest - много висок tpHigher - висок tpNormal - среден tpLower - нисък tpLowest - много нисък tpIdle - нишката се изпълнява, когато системата е неактивна Не трябва да задавате високи приоритети за нишки, ако това не се изисква от задачата, тъй като натоварва много системата. Нова тема.Резюме; Е, всъщност стартирането на темата.
Мисля, че сега разбирате как се създават нишки. Забележете, нищо сложно. Но не всичко е толкова просто. Изглежда - пишем всеки код в метода Execute и това е, но не, нишките имат едно неприятно свойство - те не знаят нищо един за друг. И какво е? - ти питаш. И ето какво: да кажем, че се опитвате да промените свойството на някакъв компонент във формата от друга нишка. Както знаете, VCL е еднонишков, целият код в приложението се изпълнява последователно. Да предположим, че по време на работа някои данни във VCL класовете са се променили, системата отнема време от основната нишка, предава я на другите нишки и я връща обратно, докато изпълнението на кода продължава от мястото, където е спряло. Ако променим нещо от нашата нишка, например във формуляр, много механизми вътре във VCL се активират (нека ви напомня, че изпълнението на основната нишка все още е "спряно"), съответно през това време те ще имат времевсички данни ще се променят. И тогава изведнъж отново се дава време на основната нишка, тя тихо продължава своето изпълнение, но данните вече са променени! До какво може да доведе това е невъзможно да се предвиди. Можете да го проверите хиляди пъти и нищо няма да се случи, а хиляда и първия път програмата ще се срине. И това се отнася не само за взаимодействието на допълнителните нишки с основната, но и за взаимодействието на нишките помежду си. Разбира се, невъзможно е да се пишат толкова ненадеждни програми.
Така стигаме до един много важен въпрос - синхронизиране на нишки.
procedure TNewThread.Execute; var i: integer; begin for i:=0 to 100 do begin sleep(50); Напредък:=i; Синхронизиране (SetProgress); край; край;
procedure TNewThread.SetProgress; begin Form1.ProgressBar1.Position:=Progress; end;
Сега ProgressBar се движи и е доста безопасно. Ето защо е безопасно: процедурата Synchronize временно спира изпълнението на нашата нишка и прехвърля контрола към основната нишка, т.е. SetProgress се изпълнява в основната нишка. Това е нещо, което трябва да запомните, защото някои хора правят грешката да извършват продължителна работа вътре в Synchronize, което очевидно кара формата да виси за дълго време. Затова използвайте Синхронизиране за показване на информация - като напредък на преместване, актуализиране на заглавия на компоненти и т.н.
Може би сте забелязали, че вътре в цикъла използваме процедурата Sleep. В еднонишково приложение Sleep се използва рядко, но в нишките е много удобно да се използва. Пример е безкраен цикъл, докато не бъде изпълнено условие. Ако не вмъкнем Sleep там, просто ще натоварим системата с безполезна работа.
Надявам се, че разбирате как работи синхронизирането. Но има друг доста удобен начин за предаване на информация във формуляр -изпращане на съобщение. Нека да разгледаме и това. За да направите това, декларирайте константа: const PROGRESS_POS = WM_USER+1;
TForm1 = class(TForm) Button1: TButton; Лента за напредък1: Лента за напредък; procedure Button1Click(Sender: TObject); частна процедура SetProgressPos(var Msg: TMessage); съобщение PROGRESS_POS; публичен < Публични декларации > край; .
procedure TForm1.SetProgressPos(var Msg: TMessage); begin ProgressBar1.Position:=Msg.LParam; end;
Сега леко ще променим, дори ще опростим, изпълнението на метода Execute на нашата нишка: procedure TNewThread.Execute; var i: integer; begin for i:=0 to 100 do begin sleep(50); Изпращане на съобщение(Form1.Handle,PROGRESS_POS,0,i); край; край;
С помощта на функцията SendMessage изпращаме съобщение до прозореца на приложението, един от параметрите на който съдържа необходимия прогрес. Съобщението се поставя в опашка и според тази опашка ще бъде обработено от основната нишка, където ще се изпълни методът SetProgressPos. Но има едно предупреждение: SendMessage, както в случая със Synchronize, ще спре изпълнението на нашата нишка, докато основната нишка не обработи съобщението. Ако това не се случи с помощта на PostMessage, нашата нишка ще изпрати съобщението и ще продължи работата си и няма значение кога ще бъде обработено там. Коя от тези функции да използвате зависи от вас, всичко зависи от задачата.
Тук по принцип разгледахме основните начини за работа с VCL компоненти от потоци. Но какво ще стане, ако в нашата програма няма една нова нишка, а няколко? И трябва да организирате работа със същите данни? Тук на помощ идват други методи за синхронизация. Ще разгледаме един от тях. За да го реализирате, трябва да добавите модула SyncObjs към проекта.
Най-интересният начинмоята гледна точка е критични секции
Те работят по следния начин: само една нишка може да работи в критичната секция, други чакат нейното завършване. За по-добро разбиране навсякъде те се сравняват с тясна тръба: представете си, потоци „тълпа“ от едната страна, но само един може да „пропълзи“ в тръбата и когато „пропълзи“, вторият ще започне да се движи и така нататък по ред. Още по-лесно е да разберете това с пример и същия ProgressBar. Така че, стартирайте един от примерите, дадени по-рано. Щракнете върху бутона, изчакайте няколко секунди и след това щракнете отново. Какво се случва? m1; CriticalSection: TCriticalSection;
procedure TForm1.FormCreate(Sender: TObject); begin CriticalSection:=TCriticalSection.Create; end;
TCriticalSection има два метода, от които се нуждаем, Enter и Leave, съответно входът и изходът от него. Нека поставим нашия код в критичната секция: procedure TNewThread.Execute; var i: integer; begin CriticalSection.Enter; за i:=0 до 100 направете започнете заспиване(50); Изпращане на съобщение(Form1.Handle,PROGRESS_POS,0,i); край; CriticalSection.Leave; край;
Опитайте да стартирате приложението и да докоснете бутона няколко пъти и след това пребройте колко пъти напредвате. Ясно ли е какъв е смисълът? Първият път, като щракнете върху бутона, създаваме нишка, тя заема критична секция и започва да работи. Натискаме второто - създава се втората нишка, но критичната секция е заета и чака докато първата я пусне. Третият, четвъртият - всички ще преминат само на свой ред.
Критичните секции са полезни заобработка на едни и същи данни (списъци, масиви) от различни нишки. След като разберете как работят, винаги ще намерите приложение за тях.
Тази кратка статия не обхваща всички методи за синхронизация, има и събития (TEvent), както и системни обекти като мютекси (Mutex), семафори (Semaphore), но те са по-подходящи за взаимодействие между приложения. Останалото, по отношение на използването на класа TThread, можете да разберете сами, в помощ "e всичко е описано подробно. Целта на тази статия е да покаже на начинаещите, че всичко не е толкова трудно и страшно, основното е да разберете какво е какво. И повече практика е най-важният опит!