Tutorial - Rad sa Thread-ovima Da napomenem na samom pocetku da sledeci tekst sadrzi samo moja iskustva, i rezultate dobijene empirijskim putem. Iako su niti (threads) u Delphi-ju dokumentoveni, Delphi Help nece da pruzi dovoljno informacija da bi moglo da se pocne sa programiranjem threadova. Za pocetak je potrebno objasniti sta su niti i cemu sluze. Svaki program sadrzi barem jednu nit (Main Thread). Zamislite sada program koji u jednom momentu mora da izvrsi neku operaciju koja traje par sekundi, ili cak minuta. Jasno je da ce program, dok traje ta operacija, biti zamrznut (u ekstremnim uslovima ce i interfejs biti zamrznut, ili pretvoren u beli kvadrat, tj. ni grafika nece da se osvezava). Dolazimo do ideje da bi bilo zgodno da se formira nezavistan proces koji ce da obavi spornu operaciju, i da za to vreme moze da se nastavi rad u programu (makar update interfejsa, ili update nekog status bara). Ti paralelni procesi se nazivaju nitima (threads). Delphi poseduje mogucnost rada sa nitima. Da ne bi bilo sve tako jednostavno, potrebno je od samog pocetka reci da VCL komponente nisu thread-safe, tj. jedna instanca VCL komponente moze primati poruke od samo jedne niti. Primera radi, jedna labela na Main formi moze primati poruke samo od Main Threada. Ukoliko se malo porazmisli, shvata se ogranicenost ovakve konstrukcije. Postoje tehnike koje prevazilaze ova ogranicenja, i njih cemo uvesti od samog pocetka, iako ce to mozda nekom otezati ucenje iz ovog tutorijala. Napravimo, za pocetak, jedan mali thread: unit testThread; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls, ComCtrls; type MyThread = class(TThread) private { Private declarations } protected procedure Execute; override; end; implementation procedure MyThread.Execute; begin MessageDlg('Poruka iz threada', mtInformation, [mbOK], 0); end; end. Snimimo ovaj fajl kao testThread.pas Sada je potrebno da napravimo nas glavni program, koji ce po potrebi da poziva ovaj thread. Otvorite novi projekat, formu nazovite frmMain, na formu ubacite jedno dugme i nazovite ga btTest. Unit snimite pod imenom untMain.pas. U var sekciju ubacite liniju kao dole, i u implementation ubacite red uses testThread da bi fajl koji smo malopre napisali bio prepoznat. unit untMain; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls, ComCtrls; type TfrmMain = class(TForm) btTest: TButton; private { Private declarations } public { Public declarations } end; var frmMain: TfrmMain; FirstThread: TThread; implementation {$R *.dfm} uses testThread; Sada je potrebno da uradimo proceduru koja ce pozvati nas thread. Uradimo onClick proceduru za dugme koje smo napravili na formi, i u proceduru ubacite sledeci kod: begin FirstThread := MyThread.Create(false); end; Parametar false znaci da se thread formira bez cekanja (suspend). Startovanjem prethodnog primera, i klikom na dugme sa forme, na ekranu bi trebala da se pojavi poruka formirana u threadu. Jeste da od ovoga nema neke prakticne koristi, ali je barem neki pocetak. Iz prethodnog smo videli da svaki thread mora da poseduje proceduru Execute, koja ce automatski biti pozvana kada se thread formira. Mana prethodnog primera je sto thread nece biti izbrisan iz memorije nakon zavrsetka. Ispravan kod bi bio: begin FirstThread := MyThread.Create(true); FirstThread.FreeOnTerminate := true; FirstThread.Resume; end; U cemu je sada razlika? Prvo, formirali smo thread u memoriji, ali smo ga postavili u stanje cekanja (true parametar za suspend). Svrha suspenda je da se thread ne startuje dok se ne podese i ostale varijable (parametri threada). U drugoj liniji smo naredili da se oslobodi memorija nakon zavrsetka threada, i u trecoj smo naredili da thread konacno krene da se izvrsava. Oslobadjanje memorije bi bila prva mera na koju moramo obratiti paznju pri radu sa threadovima. Da sada malo zakomplikujemo stvar: napravimo i jednu labelu na glavnoj formi, a da jedan od zadataka u threadu bude da promeni caption labele. Ukoliko to uradimo ovako: unit testThread; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls, ComCtrls; type MyThread = class(TThread) private { Private declarations } protected procedure Execute; override; end; implementation uses untMain; // <--- nova linija, sada thread postaje svestan postojanja glavne forme i njenih elemenata procedure MyThread.Execute; begin MessageDlg('Poruka iz threada', mtInformation, [mbOK], 0); untMain.frmMain.Label1.Caption:='Done'; // <--- ovo menja tekst labela end; end. Dobili ste poruku o gresci? Naravno, posto je Label VCL komponenta koja se nalazi na glavnoj formi, i prima poruke samo od Main threada. Za divno cudo, meni je ovo uspelo vise puta bez poruke o gresci (Delphi 7), ali se desavalo da komponenta na formi pobrljavi. Zarad ovih i ovakvih stvari, postoji posebna metoda prenosenja poruka izmedju threadova, i naziva se sinhronizacijom. Ispravan kod za nas thread bi bio: unit testThread; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls, ComCtrls; type MyThread = class(TThread) private { Private declarations } protected procedure Execute; override; procedure UpdateLabel; end; implementation uses untMain; // <--- nova linija, sada thread postaje svestan postojanja glavne forme i njenih elemenata procedure MyThread.Execute; begin MessageDlg('Poruka iz threada', mtInformation, [mbOK], 0); Synchronize(UpdateLabel); end; procedure MyThread.UpdateLabel; begin untMain.frmMain.Label1.Caption := 'Done'; end; end. Mislim da nije tesko shvatiti sta smo upravo uradili. Menjanje Captiona tabele smo strpali u posebnu proceduru, a tu proceduru pozivamo iz Execute procedure pomocu naredbe Synchronize. Ovo nam omogucuje da se nas thread sinhronizuje sa glavnim threadom, i da uspesno prenese poruku Labeli. Na ovaj nacin mozemo da, u nekom nasem programu, onemogucimo neke dugmice dok se thread ne zavrsi, ili da osvezavamo status bar, itd itd... Od ostalih stvari potrebnih stvari za rad sa threadovima, spomenuo bih jos jednu stvar, a to je prekidanje threada u toku izvrsavanja. To se radi pozivanjem procedure Terminate. Za nas gornji primer, to bi znacilo da negde u glavnom programu mozete postaviti FirstThread.Terminate;. Dacu vam ideju za daljnje eksperimente i razmisljanja: - formirajte jos jednu instancu naseg threada (SecondThread) i izvrsite ga istovremeno sa prvim. Da nismo uradili menjanje teksta na Labeli pomocu synchronize, onda bi oba threada pokusala istovremeno da pristupe labeli, pa bi nastao haos - uradite vise razlicitih threada, a samo jedno Cancel dugme, koje treba da prekine threadove koji se trenutno izvrsavaju. Znaci, negde morate da vodite evidenciju koji thread se izvrsava, posto ce pozivanje Terminate za thread koji se trenutno ne izvrsava rezultovati greskom. Ja sam koristio boolean varijable za svaki thread, pa kada startujem thread - setujem i varijablu. Kada stisnem Cancel, odradim Terminate samo za one za koje je odgovarajuca varijabla setovana. Pozdrav do sledeceg tutorijala