În procesul de elaborare a unei aplicații cu mai multe șuruburi, trebuie rezolvate două probleme interdependente: delimitarea accesului la resurse și blocajul. Dacă mai multe fire accesează aceeași resursă (zonă de memorie, fișier, dispozitiv) cu programare neatentă, poate exista o situație în care mai multe fire încearcă să efectueze unele manipulări cu resursele partajate. În acest caz, secvența normală de operațiuni la accesarea resursei este probabil să fie încălcată. Problema cu delimitarea accesului poate apărea chiar și cu operații foarte simple. Să presupunem că avem un program care creează mai multe fire. Fiecare fir își îndeplinește sarcina și apoi se termină. Vrem să controlam numărul de thread-uri active la un moment dat și, în acest scop, să introducem contorul de fir - variabila globală Counter. Procedura fluxului poate arăta astfel: procedura MyThread.Execute;
începe
Inc (Counter);
.
Dec (Contor);
se încheie;
O problemă asociată cu acest cod este faptul că procedurile Inc și dec nu atomică (de exemplu, procedura Inc necesare pentru a efectua trei instrucțiuni procesor - Load contravaloare într-un registru al procesorului, pentru a efectua creștere în sine, și apoi scrie rezultatul regiștrilor procesorului în zona de memorie Contor). Este ușor de imaginat că în cazul în care două fire de a încerca să efectueze simultan procedura de valoare Inc, Contor poate fi majorat cu 1 în loc de 2. O astfel de eroare este dificil de detectat atunci când depanare, deoarece probabilitatea de eroare nu este egal cu unitatea. Valul se poate întâmpla astfel încât în timpul testului de funcționare al programului totul să funcționeze bine și clientul va găsi deja eroarea.
Soluția problemei poate fi folosirea unor funcții speciale. De exemplu, în locul procedurii Inc, puteți utiliza procedura InterlockedIncrement pentru interfața Win API și în loc de Dec-InterlockedDecrement. Aceste proceduri asigură că, în orice moment, nu mai mult de un fir va avea acces la variabila contra.
În general, blocarea este utilizată pentru a rezolva problema partajării accesului, mecanisme speciale care asigură că la fiecare moment dat un singur fir are acces la o anumită resursă. Într-un moment în care o resursă este blocată de un fir, executarea altor fire care încearcă să acceseze resursa este suspendată. Cu toate acestea, folosirea încuietorilor creează o altă problemă - blocări. Un impas apare atunci când fluxul A blocuri de resurse necesare pentru a continua să lucreze fluxul B și resurse necesare pentru continuarea fluxului de lucru A. blocuri fluxul B Din moment ce nici un fir de execuție poate continua resursele blocate nu pot fi eliberate, rezultând sau se blochează fluxuri.
Fire în Delphi 6
Dintre toate implementările considerate, implementarea firelor în Delphi 6 este cea mai simplă. Aceasta este baza pe care versiunile ulterioare construiesc un model mai complex de fluxuri de interacțiune.
Delphi 6, precum și în alte versiuni ale Delphi, ca funcția de curgere este utilizată funcția ThreadProc modulului Clase, care, la rândul său, determină obiectul TThread metoda Execute: ThreadProc Funcția (Subiect: TThread): Integer;
var
FreeThread: Boolean;
începe
încerca
dacă nu este Thread.Terminated then
încerca
Thread.Execute;
cu excepția
Thread.FFatalException: = AcquireExceptionObject;
se încheie;
în cele din urmă
FreeThread: = Thread.FreeOnTerminate;
Rezultat: = Thread.FReturnValue;
Thread.Finished: = Adevărat;
Thread.DoTerminate;
dacă FreeThread apoi Thread.Free;
EndThread (Rezultat);
se încheie;
se încheie;
Structura TSyncProc arată astfel: TSyncProc = înregistrare
Subiect: TThread;
Semnal: Thandle;
se încheie;
Conține informații despre firul care a declanșat sincronizarea și identificatorul de semnal utilizat pentru sincronizarea sincronizării (îmi pare rău pentru cuvântul neintenționat).
Este important să se sublinieze că metoda Synchronize nu este doar sincronizeaza executarea a trecut la metoda aceasta metoda cu celelalte metode din fluxul principal, dar, de asemenea, să organizeze punerea în aplicare a metodei în contextul firul principal. Aceasta este, de exemplu, variabilele globale declarate ca threadvar atunci când se execută Metoda vor avea valorile atribuite acestora în firul principal, mai degrabă decât cele atribuite acestora în firul care a numit Sincronizare.
Sincronizarea nu este suficientă pentru sincronizarea normală a firelor principale și secundare ale metodei Sincronizare. Imaginați-vă această situație: în metoda curentului principal, ne așteptăm finalizarea fluxului secundar (indiferent în ce mod, ceea ce este important este faptul că această metodă nu se întoarce controlul la fluxul principal, atâta timp cât fluxul secundar este completă), și un flux secundar în acest moment este Sincronizați metoda. Ca rezultat, un blocaj se produce: metoda de curgere principală nu poate fi finalizată până când debitul secundar este finalizat, iar fluxul secundar nu poate fi finalizată până metoda Synchronize se va face (și în acest scop, fluxul principal a revenit la ciclul de procesare a mesajului). Pentru a rezolva această situație, există funcția CheckSynchronize, ceea ce duce la un apel de a pune în aplicare toate metodele care sunt în prezent în coada de așteptare SyncList, și să controleze întoarcerea tuturor metodei Sincronizați, numit flux secundar. În Delphi 6, această funcție este implementată după cum urmează: funcția CheckSynchronize: Boolean;
var
SyncProc: PSyncProc;
începe
dacă GetCurrentThreadID <> MainThreadID atunci
ridicați EThread.CreateResFmt (@SCheckSynchronizeError, [GetCurrentThreadID]);
dacă este ProcPosted atunci
începe
EnterCriticalSection (ThreadLock);
încerca
Rezultat: = (SyncList <> nil) și (SyncList.Count> 0);
dacă Rezultatul atunci
începe
în timp ce SyncList.Count> 0 face
începe
SyncProc: = SyncList [0];
SyncList.Delete (0);
încerca
SyncProc.Thread.FMethod;
cu excepția
SyncProc.Thread.FSynchronizeException: = AcquireExceptionObject;
se încheie;
SetEvent (SyncProc.signal);
se încheie;
ProcPosted: = False;
se încheie;
în cele din urmă
LeaveCriticalSection (ThreadLock);
se încheie;
alt rezultat Rezultat: = False;
se încheie;
După cum puteți vedea, funcția CheckSynchronize este simplu: se verifică valoarea ProcPosted, iar în cazul în care această valoare la True, elimină în mod constant înregistrările din coadă SyncList efectuează metode adecvate și stabilește semnalele corespunzătoare. Rețineți că funcția verifică (folosind GetCurrentThreadID), de la care se numește firul. Apelarea Sincronizării nu din firul principal poate duce la haos, deci în acest caz este aruncată o excepție EThread. Pentru a gestiona apelurile metodice care sunt "încorporate" în firul principal utilizând funcția Sincronizare, ciclul principal de procesare a mesajelor de thread solicită de asemenea metoda CheckSynchronize.
O altă metodă interesantă pentru noi este metoda WaitFor a clasei TThread. Această metodă blochează execuția firului care a provocat-o până când fișierul pentru care a fost apelat obiectul WaitFor se încheie. Pur și simplu, dacă în threadul principal dorim să așteptăm finalizarea fluxului MyThread, putem apela MyThread.WaitFor;
Iată cum este implementat WaitFor în Delphi 6: funcția TThread.WaitFor: LongWord;
var
H: Thandle;
WaitResult: Cardinal;
Msg: TMsg;
începe
H: = FHandle;
dacă GetCurrentThreadID = MainThreadID atunci
începe
WaitResult: = 0;
repeta
dacă WaitResult = WAIT_OBJECT_0 + 1 atunci
PeekMessage (Msg, 0, 0, 0, PM_NOREMOVE);
Somn (0);
CheckSynchronize;
WaitResult: = MsgWaitForMultipleObjects (1, H, False, 0, QS_SENDMESSAGE);
Win32Check (WaitResult <> WAIT_FAILED);
până la WaitResult = WAIT_OBJECT_0;
termină altfel WaitForSingleObject (H, INFINITE);
CheckThreadError (GetExitCodeThread (H, Rezultat));
se încheie;
Metoda WaitFor poate fi apelată nu numai din firul principal, ci din orice altul. Dacă WaitFor numit de firul principal GetCurrentThreadID = MainThreadID, metoda determină periodic funcția CheckSynchronize pentru a preveni blocajul de mai sus, precum și golește periodic toate mesajele Windows. Dacă metoda WaitFor este solicitat de orice alt flux, care ar trebui să dețină coada de mesaje nu poate fi, el doar așteaptă semnalul de la sfârșitul fluxului controlat folosind Win API WaitForSingleObject. Aici trebuie remarcat faptul că identificatorul firului (câmpul FHandle) este un semnal concurent, care este setat de sistemul Windows când firul se termină.
Și ce se întâmplă dacă firul numește WaitFor pentru el însuși? Firul care a provocat auto.WaitFor;
va aștepta pentru totdeauna finalizarea propriu-zisă (cel puțin până când un alt thread va numi funcția "hard" TerminateThread pentru API pentru acest thread). Desigur, este puțin probabil ca un programator sensibil să scrie ceva de genul Self.WaitFor, dar situațiile pot fi mai complicate. Ciudat că dezvoltatorii Delphi nu a preveni o astfel de posibilitate, „sinucidere“, și, de fapt, face foarte simplu - trebuie doar să compare FHandle valoarea și valoarea returnată de GetCurrentThreadID.
Fire în Delphi 7
În comparație cu Delphi 6, nu există atât de multe schimbări în lucrul cu firele din Delphi 7. Luați în considerare implementarea funcției CheckSynchronize: funcția CheckSynchronize (Timeout: Integer = 0): Boolean;
var
SyncProc: PSyncProc;
LocalSyncList: TList;
începe
dacă GetCurrentThreadID <> MainThreadID atunci
ridicați EThread.CreateResFmt (@SCheckSynchronizeError, [GetCurrentThreadID]);
dacă Timeout> 0 atunci
WaitForSyncEvent (Timeout)
altfel
ResetSyncEvent;
LocalSyncList: = zero;
EnterCriticalSection (ThreadLock);
încerca
Integer (LocalSyncList): = InterlockedExchange (Integer (SyncList), Integer (LocalSyncList));
încerca
Rezultat: = (LocalSyncList <> nil) și (LocalSyncList.Count> 0);
dacă Rezultatul atunci
începe
în timp ce LocalSyncList.Count> 0 face
începe
SyncProc: = LocalSyncList [0];
LocalSyncList.Delete (0);
LeaveCriticalSection (ThreadLock);
încerca
încerca
SyncProc.SyncRec.FMethod;
cu excepția
SyncProc.SyncRec.FSynchronizeException: = AcquireExceptionObject;
se încheie;
în cele din urmă
EnterCriticalSection (ThreadLock);
se încheie;
SetEvent (SyncProc.signal);
se încheie;
se încheie;
în cele din urmă
LocalSyncList.Free;
se încheie;
în cele din urmă
LeaveCriticalSection (ThreadLock);
se încheie;
se încheie;
În noua versiune locul ProcPosted pavilion folosit management de eveniment SyncEvent, care a creat un număr de funcții: SetSyncEvent, ResetSyncEvent, WaitForSyncEvent. Metoda WaitFor utilizează evenimentul SyncEvent pentru a optimiza buclă de mesaje. Instalarea SyncEvent indică faptul că, la rândul său, o nouă metodă de așteptare pentru sincronizare și doriți să apelați CheckSynchronize.
Metoda CheckSynchronize are un parametru TimeOut care specifică cât timp trebuie să aștepte metoda pentru evenimentul SyncEvent înainte de a reveni la control. Specifică timeout convenabil în cazul în care metoda CheckSynchronize este numit într-o buclă (firul care apelează CheckSynchronize, oferind timpul său CPU cu alte fire, în loc de provocări de răsucire în așteptare), dar durata și CheckSynchronize apel metoda poate crește în mod inutil. Acordați atenție și modului în care lucrul cu coada SyncList sa schimbat în Delphi 7. În versiunile anterioare ale tuturor CheckSynchronize SyncList (capturi folosind ThreadLock) pe durata prelucrării metodelor în coada de așteptare (și de data aceasta ar putea fi relativ mare). Dar până CheckSynchronize deține SyncList obiect, operațiuni SyncList coadă, celelalte fire sunt blocate. Pentru a elibera SyncList cât mai curând posibil, păstrează un pointer la obiectul de așteptare curent (utilizând funcția Win API InterlockedExchange) într-o LocalSyncList variabilă locală și SyncList variabilă setează la zero. Apoi, accesul la variabila SyncList se deschide din nou. Acum, dacă un alt fir vrea metodă pentru a sincroniza din nou, va trebui să creați un nou SyncList obiect, dar accesul la coadă doar pentru a fi blocate pentru timpul necesar pentru indicii de schimb, astfel încât câștigurile globale de productivitate ar trebui să fie considerabile.
metoda de lucru în modul de blocare este similar cu metoda Sincronizați de lucru în Delphi 7: Sistemul creează un eveniment SyncProc.Signal, care va semnala punerea în aplicare a metodei de pe firul principal, și apoi formează SyncProc structura, care descrie metoda sincronizate adaugă această rândul său, structura SyncList, setează SyncEvent semnal și așteaptă funcția CheckSynchronize nu stabilește alarma SyncProc.Signal, indicând faptul că metoda este executată sincronizat. Pentru a descrie metoda chemată, este încă utilizată o înregistrare de tip TSyncProc, care, totuși, arată diferit: TSyncProc = record
SyncRec: PSynchronizeRecord;
Queued: Boolean;
Semnal: Thandle;
se încheie;
Câmpul SyncRec este un indicator pentru structura TSynchronizeRecord. Câmpul Queued indică dacă apelul este asincron și câmpul Semnal este utilizat atunci când apelul este blocat.
Dacă QueueEvent este trecut True, metoda de apelare este adăugată în coadă asincron. În acest caz, vom crea o nouă instanță TSyncProc de înregistrare (pentru apel asincrone nu pot folosi variabile locale, deoarece structura ar trebui să existe după finalizarea Synchronize de apel).
Dezavantaje de filetare în Delphi
Cel mai important dezavantaj este metoda utilizată pentru suspendarea și reluarea execuției fluxului. În acest scop, VCL utilizează API-urile Windows SuspendThread și ResumeThread, care sunt în general vorbite. Proiectat pentru depanare. Funcția SuspendThread poate opri execuția firului în orice moment. Firul nu poate interzice suspendarea executării unei fragmente critice de cod și nu primește o avertizare că va fi suspendată. Schimbul de mesaje între firele secundare și firul principal este bine gândit, în ultimele versiuni ale Delphi sunt adăugate chiar apeluri asincrone, dar nu există mecanisme standard pentru transferul mesajelor de la firul principal la cele secundare. Aici trebuie notat că sub "firul principal" se înțelege firul în care se execută metoda Application.Run și se procesează evenimentele. Delphi este rău pentru un model în care toate firele sunt egale.
imputernicire
dar în firul principal nu funcționează. Metoda de sincronizare, care cheamă Queue, va verifica dacă este apelată din firul principal și, în acest caz, va executa metoda MethodToExecute, fără întârziere. Deci, procedura ExecAfter: procedura ExecAfter (AMethod.ThreadMethod);
var
SyncProcPtr: PSyncProc;
SyncRecPtr: PSynchronizeRecord;
începe
Nou (SyncProcPtr);
Nou (SyncRecPtr);
SyncRecPtr.FThread: = nil;
SyncRecPtr.FMethod: = Amethod;
SyncRecPtr.FProcedure: = nil;
SyncRecPtr.FSynchronizeException: = zero;
SyncProcPtr.Signal: = 0;
EnterCriticalSection (ThreadLock);
încerca
SyncProcPtr.Queued: = Adevărat;
dacă SyncList = nul atunci
SyncList: = TList.Create;
SyncProcPtr.SyncRec: = SyncRecPtr;
SyncList.Add (SyncProcPtr);
SignalSyncEvent;
dacă este atribuită (WakeMainThread) atunci
WakeMainThread (SyncProcPtr.SyncRec.FThread);
în cele din urmă
LeaveCriticalSection (ThreadLock);
se încheie;
se încheie;
Versiunea noastră vă permite să efectuați un apel de metodă ExecAfter AMethod după eliberarea acestei proceduri, în care am numit ExecAfter (dacă se dorește este ușor să rescrie procedura de proceduri independente de apel, și fără metode). Procedurile ExecAfter de punere în aplicare ar trebui să fie amplasate în unitatea de clase, în caz contrar nu putem avea acces la variabilele necesare noi ThreadLock și SyncList. Apropo, dacă nu doriți să faceți modificări la "principala" copie a clasei, puteți folosi o copie locală pentru un anumit program. Pentru a face acest lucru, copiați fișierul Classes.pas modificat în directorul de proiect. Acum, dacă adăugați o secvență la program: TForm1.Method1;
începe
ExecAfter (Metoda2);
method3;
se încheie;
Metoda 2 va fi executată după metoda 3 (și după ieșirea din metoda 1).