Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp...

27
1 Fire de execuţie În acest capitol vom discuta conceptul de fir de execuţie şi vom descrie modul în care sistemul foloseşte fire de execuţie pentru a executa codul aplicaţiei noastre. La fel ca şi procesele, firele au proprităţi asociate lor şi vom discuta despre unele dintre funcţiile disponibile pentru interogarea şi schimbarea acestor proprietăţi. De asemenea vom examina funcţiile care ne permit să creăm să creăm noi fire in sistem. În final vom vorbi despre terminarea frelor de execuţie. Când creăm un fir de execuţie Un fir de execuţie descrie o cale de execuţie în interiorul unui proces. De fiecare dată când un proces este iniţializat, sistemul creează un fir de execuţie principal. Acest fir porneşte codul C run-time de pornire, care la rândul lui apelează funcţia noastră WinMain, apoi continuă să se execute până când funcţia WinMain returnează şi codul C run-time de pornire apelează ExitProcess. Pentru multe aplicaţii, firul principal de execuţie este singurul fir de execuţie de care are nevoie aplicaţia. Totuşi, procesele pot crea fire suplimentare pentru a le ajuta să îşi îndeplinească sarcinile. Ideea din spatele creării firelor de execuţie este de a utiliza cât mai mult timp de procesor. De exemplu, un program care lucrează cu foi de calcul are nevoie să efectueze recalculări pe măsură ce utilizatorul introduce date în celule. Deoarece recalculările unei foi de calcul complexe pot avea de nevoie de câteva secunde pentru a fi efectuate, o aplicaţie bine gândită nu ar trebui să recalculeze foaia de calcul după fiecare schimbare pe care o efectuează utilizatorul. În schimb, recalcularea ar trebui făcută într-un fir de execuţie cu prioritate mai redusă decât firul principal de execuţie. În acest mod, dacă utilizatorul introduce date, firul principal rulează, lucru care înseamnă că sistemul nu va programa nici un timp de calcul firului care efectuează recalcularea. Atunci când utilizatorul se opreşte din scris, firul principal este suspendat, aşteptând o intrare iar firului care face recalcularea îi este dat timp de lucru. De îndată ce utilizatorul începe să scrie din nou, firul principal de execuţie, având prioritate mai mare, trece in faţa firului cu recalcularea. Crearea firelor de execuţie adiţionale face ca aplicaţia să devină „înţelegătoare” cu utilizatorul. De asemenea este destul de uşor de implementat. Într-un exemplu similar, putem crea un fir de execuţie suplimentar pentru o funcţie de repaginare într-un procesor de texte care trebuie să repagineze textul pe măsură ce utilizatorul introduce text în document. Microsoft Word pentru Windows, de exemplu, trebuie să simuleze un comportament multifiliar în Windows pe 16 biţi dar poate foarte uşor să creeze un nou fir dedicat repaginării documentului în Win32. Firul principal de execuţie va fi responsabil cu procesarea intrărilor utilizatorului, iar firul din fundal va fi responsabil cu localizarea salturilor la pagină nouă. Este de asemenea folositor pentru a crea un fir de execuţie separat pentru a trata tipărirea în cadrul unei aplicaţii. În acest mod, utilizatorul poate continua să folosească aplicaţia în timpul tipăririi. În plus, atunci când efectuăm o operaţie care într-o perioadă mare de timp, multe aplicaţii afişează o căsuţă de dialog care permite utilizatorului să oprească operaţia. De exemplu, atunci când Explorer-ul copie nişte fişiere, afişează o căsuţă de dialog care, în afara faptului că arată progresul operaţiei, de asemenea conţine un buton Cancel. Dacă apăsăm acest buton în timp ce fişierele sunt copiate, oprim operaţia.

Transcript of Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp...

Page 1: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

1

Fire de execuţie În acest capitol vom discuta conceptul de fir de execuţie şi vom descrie modul în care sistemul foloseşte fire de execuţie pentru a executa codul aplicaţiei noastre. La fel ca şi procesele, firele au proprităţi asociate lor şi vom discuta despre unele dintre funcţiile disponibile pentru interogarea şi schimbarea acestor proprietăţi. De asemenea vom examina funcţiile care ne permit să creăm să creăm noi fire in sistem. În final vom vorbi despre terminarea frelor de execuţie.

Când creăm un fir de execuţie

Un fir de execuţie descrie o cale de execuţie în interiorul unui proces. De fiecare dată când un proces este iniţializat, sistemul creează un fir de execuţie principal. Acest fir porneşte codul C run-time de pornire, care la rândul lui apelează funcţia noastră WinMain, apoi continuă să se execute până când funcţia WinMain returnează şi codul C run-time de pornire apelează ExitProcess. Pentru multe aplicaţii, firul principal de execuţie este singurul fir de execuţie de care are nevoie aplicaţia. Totuşi, procesele pot crea fire suplimentare pentru a le ajuta să îşi îndeplinească sarcinile. Ideea din spatele creării firelor de execuţie este de a utiliza cât mai mult timp de procesor.

De exemplu, un program care lucrează cu foi de calcul are nevoie să efectueze recalculări pe măsură ce utilizatorul introduce date în celule. Deoarece recalculările unei foi de calcul complexe pot avea de nevoie de câteva secunde pentru a fi efectuate, o aplicaţie bine gândită nu ar trebui să recalculeze foaia de calcul după fiecare schimbare pe care o efectuează utilizatorul. În schimb, recalcularea ar trebui făcută într-un fir de execuţie cu prioritate mai redusă decât firul principal de execuţie. În acest mod, dacă utilizatorul introduce date, firul principal rulează, lucru care înseamnă că sistemul nu va programa nici un timp de calcul firului care efectuează recalcularea. Atunci când utilizatorul se opreşte din scris, firul principal este suspendat, aşteptând o intrare iar firului care face recalcularea îi este dat timp de lucru. De îndată ce utilizatorul începe să scrie din nou, firul principal de execuţie, având prioritate mai mare, trece in faţa firului cu recalcularea. Crearea firelor de execuţie adiţionale face ca aplicaţia să devină „înţelegătoare” cu utilizatorul. De asemenea este destul de uşor de implementat.

Într-un exemplu similar, putem crea un fir de execuţie suplimentar pentru o funcţie de repaginare într-un procesor de texte care trebuie să repagineze textul pe măsură ce utilizatorul introduce text în document. Microsoft Word pentru Windows, de exemplu, trebuie să simuleze un comportament multifiliar în Windows pe 16 biţi dar poate foarte uşor să creeze un nou fir dedicat repaginării documentului în Win32. Firul principal de execuţie va fi responsabil cu procesarea intrărilor utilizatorului, iar firul din fundal va fi responsabil cu localizarea salturilor la pagină nouă.

Este de asemenea folositor pentru a crea un fir de execuţie separat pentru a trata tipărirea în cadrul unei aplicaţii. În acest mod, utilizatorul poate continua să folosească aplicaţia în timpul tipăririi. În plus, atunci când efectuăm o operaţie care într-o perioadă mare de timp, multe aplicaţii afişează o căsuţă de dialog care permite utilizatorului să oprească operaţia. De exemplu, atunci când Explorer-ul copie nişte fişiere, afişează o căsuţă de dialog care, în afara faptului că arată progresul operaţiei, de asemenea conţine un buton Cancel. Dacă apăsăm acest buton în timp ce fişierele sunt copiate, oprim operaţia.

Page 2: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

2

În Windows pe 16 biţi, implementarea acestui tip de funcţionalitate are nevoie de apeluri periodice ale funcţiei PeekMessage în interiorul cozii de mesaje a copierii fişierelor. Apelurile PeekMessage pot fi făcute doar între citirea şi scrierea unui fişier. Dacă citim un block mare de date, răspunsul la apăsarea butonului nu apare până când blocul nu este citit. Dacă fişierul este citi de pe o unitate de dischetă, acest lucru poate dura câteva secunde. Deoarece timpul de răspuns este atât de lent, putem apăsa de mai multe ori pe un astfel de buton crezând că sistemul nu ştie că am oprit operaţia.

Punând copierea fişierului într-un fir de execuţie separat, nu trebuie să împrăştiem apeluri ale funcţiei PeekMessage prin codul nostru – firul nostru operează independent. Acest lucru înseamnă că o apăsare a butonului Cancel are un efect imediat.

Putem de asemenea să folosim fire de execuţie pentru a crea aplicaţii care simulează evenimente din lumea reală. Un exemplu este o simulare a unui supermarket. Deoarece fiecare cumpărător este reprezentat de propriul fir de execuţie, teoretic fiecare este independent unul de altul şi poate intra în magazin şi poate ieşi oricând crede de cuviinţă. Simularea poate monitoriza aceste activităţi pentru a determina cât de bine funcţionează supermarket-ul.

Deşi putem rula simulări, există probleme potenţiale. În primul rând, în mod ideal am vrea ca fiecare fir de execuţie să fie executat de către un procesor propriu. Deoarece nu sete o soluţie practică să ne aşteptăm să avem un procesor pentru fiecare fir de execuţie, soluţia este de a atribui un timp maxim după care sistemul să pauzeze execuţia unui fir, permiţând execuţia altuia. De exemplu, dacă simularea noastră are două fire de execuţie şi maşina pe care rulează are 8 procesoare, sistemul poate atribui fiecare fir unui procesor diferit. Totuşi, dacă simularea are 1000 de fire de execuţie, sistemul va trebui să le atribuie continuu câte unul din cele 8 procesoare. Totuşi poate apărea o suprasolicitare atunci când sistemul programează un număr mare de fire pentru un număr redus de procesoare. Dacă aplicaţia noastră are un timp de execuţie mai mare, suprasolicitarea are un impact relativ scăzut asupra simulării. Totuşi, dacă simularea se face într-o perioadă scurtă de timp, suprasarcina poate lua un procent mai mare din timpul total de execuţie al aplicaţiei.

În al doilea rând, sistemul în sine are nevoie de unele fire de execuţie care să ruleze în timp ce alte procese sunt în execuţie. Toate aceste fire de execuţie trebuie să fie programate pentru timp de procesor la rândul lor, lucru care în mod sigur va afecta rezultatele aplicaţiei.

Şi în al treilea rând, simularea este folositoare doar dacă păstrăm evidenţa progresului ei. De exemplu, simularea supermarket-ului adaugă intrări într-o listbox pe măsură ce cumpărătorii efectuează diferite acţiuni; adăugarea acestor intrări ia din timpul aplicaţiei. Principiul de Nesiguranţă al lui Heisenberg afirmă ca determinarea mai precisă a unei cantităţi are ca efect calcularea mai precisă a altora. Acest principiu este în mod sigur adevărat în cazul de faţă.

Când nu trebuie să creeăm un fir de execuţie

Prima dată când la majoritatea programatorilor le este dat accesul la un mediu care suportă fire de execuţie multiple, ei sunt extaziaţi. Dacă ei ar fi avut firele de execuţie mai devreme, aplicaţiile lor ar fi fost mai uşor de scris. Şi, pentru un motiv necunoscut, aceşti programatori încep divizarea unei aplicaţii în piese individuale, fiecare putând fi executată ca propriul său fir de execuţie. Acesta nu este modul corect de dezvoltare a unei aplicaţii.

Page 3: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

3

Firele sunt incredibil de folositoare şi îşi au locul lor, dar atunci când folosim fire de execuţie putem crea noi potenţiale probleme în timp ce încercăm să rezolvăm pe cele vechi. De exemplu, să spunem că dezvoltăm a aplicaţie care procesează cuvinte si vrem să permitem ca funcţia de tipărire să ruleze ca propriul fir de execuţie. Acest lucru pare o idee bună deoarece utilizatorul poate imediat să se întoarcă la editarea documentului în timp ce acesta este tipărit. Dar totuşi acest lucru înseamnă că datele din document pot fi schimbate în timp ce documentul este tipărit. Acest lucru creează un nou tip de problemă pe care trebuie să o rezolvăm. Totuşi poate ar fi mai bine să nu lăsăm tipărirea în propriul fir de execuţie; dar această „soluţie” este puţin mai drastică. Ce ar fi dacă am permite utilizatorului să editeze un alt document dar să blocăm documentul care trebuie tipărit astfel încât să nu poată fi modificat până când procesul de tipărire nu s-a terminat ? Cea de-a treia soluţie este : copiem fişierul de tipărit într-un fişier temporar şi lăsăm utilizatorul să îl modifice pe cel original. Atunci când fişierul temporar care conţine documentul s-a terminat de tipărit, putem şterge fişierul temporar.

După cum se observă, firele de execuţie rezolvă unele probleme cu riscul creării altora noi. O altă utilizare greşită a firelor de execuţie poate apărea în dezvoltarea interfeţei utilizator a unei aplicaţii. În majoritatea aplicaţiilor, toate componentele interfeţei utilizator ar trebui să împartă acelaşi fir de execuţie. Dacă creăm o căsuţă de dialog, de exemplu, nu ar avea nici un sens ca un listbox să fie creat de un fir de execuţie şi un buton de alt fir.

Să mergem puţin mai departe şi să spunem că avem propriul control listbox care sortează datele de fiecare dată este adăugat sau şters un element. Operaţia de sortare poate dura câteva secunde, aşa că decidem să îi atribuim acestui control un fir de execuţie propriu. În acest mod, utilizatorul poate continua să lucreze cu alte controale în timp ce firul controlului listbox continuă să lucreze.

Procedând în acest mod nu este o ideea foarte buna, totuşi. În primul rând, fiecare fir de execuţie care creează o fereastră trebuie să conţină de asemenea o buclă GetMessage. În al doilea rând, firul listbox-ului conţine propria coadă GetMessage, am putea să avem nişte probleme de sincronizare între firele de execuţie. Putem rezolva aceste probleme atribuind controlului listbox un fir dedicat al cărui singur scop este de a sorta elementele în fundal.

În puţine situaţii, atribuind fire individuale obiectelor de interfaţă utilizator este folositoare. În sistem, fiecare proces are propriul fir de execuţie care controlează propria interfaţă utilizator. De exemplu, aplicaţia Calculator are un fir de execuţie care tratează toate ferestrele aplicaţiei, iar aplicaţia Paint are propriul fir de execuţie care creează şi gestionează ferestrele proprii ale Paint-ului. Aceste fire separate au fost create pentru protecţie şi pentru robusteţe. Dacă firul de execuţie al Calculator-ului intră într-o buclă infinită, problema rezultată nu are nici un efect asupra firului de execuţie al Paint-ului. Acest comportament este destul de diferit de ceea ce vedem în Windows pe 16 biţi. În Windows pe 16 biţi, dacă o aplicaţie se blochează, întregul sistem se blochează. Sistemele Win32 ne permit să ieşim din aplicaţia Calculator (chiar dacă aceasta s-a blocat) şi să începem folosirea aplicaţiei Paint.

Probabil că cel mai bun exemplu de aplicaţie care creează ferestre cu fire multiple de execuţie este Explorer-ul. Dacă utilizatorul lucrează cu o fereastră Explorer, să spunem unitatea de disc C:\, iar unul dintre firele de execuţie al acestei ferestre intră într-o buclă infinită, utilizatorul nu poate să mai folosească firul pentru această fereastră dar poate totuşi să folosească alte ferestre Explorer. După cum observăm, această facilitate este folositoare deoarece utilizatorilor nu le place când shell-ul sistemului de operare nu mai răspunde la comenzi.

Page 4: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

4

O altă întrebuinţare pentru firele multiple din componentele GUI este în aplicaţiile cu interfaţă documente multiple MDI în care fiecare fereastră copil MDI rulează propriul fir de execuţie. Dacă unul din firele de execuţie fiu intră într-o buclă infinită sau începe o procedură mare consumatoare de timp, utilizatorul poate schimba la altă fereastră MDI fiu şi poate începe să lucreze, iar celălalt fir MDI fiu să rămână blocat. Acest lucru poate fi de fapt foarte folositor, că Win32 oferă o funcţie specială a cărei rezultat este similar cu crearea unei ferestre MDI fiu trimiţând mesajul WM_MDICREATE către o fereastră MDI client.

HWND CreateMDIWindow ( LPTSTR lpszClassName, LPTSTR lpszWindowName,

DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hwndparent, HINSTANCE hInst, LONG lParam);

Singura diferenţă este că funcţia CreateMDIWindow permite ca MDI-ul fiu să fie creat cu propriul fir de execuţie.

Morala este că trebuie să folosim în mod logic multiplicarea firelor. Nu trebuie să le folosim doar pentru că putem face acest lucru. Putem scrie multe aplicaţii folositoare şi puternice folosind doar firul principal de execuţie atribuit procesului.

Firele de execuţie în MFC

MFC încapsulează fire de execuţie în clasa CWinThread. De asemenea încapsulează evenimente, mutex-uri si alte obiecte de sincronizare Win32 in clase C++ uşor de folosit. Face MFC-ul multiplicarea firelor mai uşoară ? Nu chiar. Dezvoltatorii de software care au scris aplicaţii Windows cu multiple fire de execuţie sunt adeseori surprinşi să afle că MFC-ul adaugă complexităţi proprii. Secretul scrierii unui program cu multiple fire de execuţie în MFC este de a înţelege foarte bine ceea ce faci şi de a şti unde pot apărea probleme.

Din punctul de vedere al Windows-ului toate firele de execuţie sunt la fel. Totuşi, MFC-ul distinge două tipuri de fire de execuţie : fire de execuţie cu interfaţă cu utilizatorul şi fire de execuţie de lucru. Diferenţa dintre cele două este ca primele pot crea ferestre si pot procesa mesaje trimise către aceste ferestre. Cel de-al doilea tip de fire de execuţie efectuează operaţii în fundal care nu primesc o intrare directă de la utilizator şi de aceea nu au nevoie de ferestre si de cozi de mesaje.

Sistemul în sine furnizează două exemple foarte bune despre modul în care firele de execuţie cu interfaţă cu utilizatorul si firele de execuţie de lucru pot fi folosite. Atunci când deschidem un director în shell-ul sistemului de operare, shell-ul lansează un fir de execuţie cu interfaţă cu utilizatorul care creează o fereastră care arată conţinutul directorului. Dacă copiem prin „drag and drop” un grup de fişiere în directorul pe care l-am deschis, firul de execuţie al acelui director efectuează transferurile de fişiere. (Uneori acesta creează un alt fir de execuţie – de data aceasta un fir de execuţie de lucru pentru copierea fişierelor. ) Avantajele acestei arhitecturi cu multiple fire de execuţie este că, de îndată ce copierea a început, utilizatorul poate comuta în ferestrele deschise în alte directoare şi poate continua să lucreze în timp ce fişierele sunt copiate în fundal. Lansarea unui fir de execuţie cu interfaţă cu utilizatorul care creează o fereastră este conceptual similar lansării unei aplicaţii în interiorul unei aplicaţii. Cea mai des folosită întrebuinţare pentru acest tip de fire de execuţie este pentru crearea mai multor ferestre deservite de fire de execuţie separate.

Page 5: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

5

Firele de execuţie de lucru sunt ideale pentru efectuarea unor operaţii izolate care pot fi despărţite de restul aplicaţiei si pot fi efectuate în fundal. Un exemplu clasic de fir de execuţie de lucru este cel folosit de un control de animaţie pentru a rula clipuri cu extensia .AVI. Acel fir de execuţie face mai mult decât să deseneze un cadru, se pune in starea de sleep pentru o fracţiune de secundă iar apoi se „trezeşte” şi repetă procesul. În acest mod el adaugă doar puţin la încărcătura de lucru a procesorului deoarece îşi petrece majoritatea vieţii suspendat între cadre şi totuşi asigură un serviciu de valoare. Acesta este un exemplu foarte bun de proiectare cu multiple fire de execuţie deoarece firul de execuţie din fundal primeşte de făcut o anumită operaţie şi apoi îi este permis să repete acea operaţie până când firul principal de execuţie anunţă că operaţia se poate termina.

Funcţia de fir de execuţie în SDK

Toate firele de execuţie îşi încep execuţia la o funcţie care trebuie să o precizăm în prealabil. Funcţia trebuie să aibă următorul prototip :

DWORD WINAPI YourThreadFunc (

LPVOID lpvThreadParm);

La fel ca şi WinMain, funcţia nu este apelată chiar de sistemul de operare. În schimb, acesta apelează o funcţie internă, care nu face parte din funcţiile C run-time, conţinută în KERNEl32. Putem să o numim StartOfThread; numele real nu este important. Această funcţie arată astfel : void StartOfThread (LPTHREAD_START_ROUTINE lpStartAddr,

LPVOID lpvThreadParm) { try

{ DWORD dwThreadExitCode = lpStartAddr (lpvThreadParm); ExitThread (dwThreadExitCode); }

__except (UnhandledExceptionFilter (GetExceptionInformation())) {

ExitProcess (GetExceptionCode()); } }

Funcţia StartOfThread pune în mişcare următoarele acţiuni : 1. Este setat un frame structurat de tratare a erorilor în jurul funcţiei

noastre a firului de execuţie astfel încât orice excepţie care apare în timpul execuţiei firului va avea o tratare implicită din partea sistemului.

2. sistemul apelează funcţia noastră a firului de execuţie, transmiţându-i parametrul pe 32 biţi lpvThreadParm pe care l-am transmis funcţiei CreateThread.

3. Atunci când funcţia firului returnează, funcţia StartOfThread apelează ExitThread, transmiţându-i valoarea de retur a funcţiei firului. Contorul de utilizare al obiectului nucleu este decrementat iar firul îşi întrerupe execuţia.

4. Dacă firul determină apariţia unei excepţii care nu este tratată, atunci frame-ul SEH setat de funcţia StartOfThread va trata excepţia. În mod normal, acest lucru înseamnă că o căsuţă de dialog este prezentată utilizatorului iar atunci când

Page 6: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

6

acesta închide această căsuţă, funcţia StartOfThread apelează ExitProcess pentru a termina execuţia întregului proces, nu doar firul în cauză.

Firul principal de execuţie al unui proces începe executând funcţia StartOfThread a sistemului. Aceasta apelează la rândul ei codul C run-time de startup, care apelează funcţia noastră WinMain. Codul C run-time de startup, totuşi, nu se întoarce în funcţia StartOfThread deoarece codul de startup apelează în mod explicit ExitProcess.

Funcţia firului de execuţie în MFC

O funcţie de fir de execuţie este o funcţie cu apel invers, aşa că ea trebuie să fie o funcţie statică membră a unei clase sau o funcţie globală declarată în afara unei clase. Prototipul ei este :

UINT ThreadFunc (LPVOID pParam);

pParam este o valoare pe 32 de biţi a cărei valoare este egală cu parametrul pParam transmis funcţiei AfxBeginThread. Foarte des, pParam este adresa unei structuri de date definită de aplicaţie care conţine informaţia transmisă firului de execuţie de lucru de către firul care l-a creat. De asemenea poate fi o valoare scalară, un handle sau chiar un pointer către un obiect. Este posibil să folosim aceeaşi funcţie de fir de execuţie pentru două sau mai multe fire de execuţie dar trebuie să fim atenţi la problemele de reintrare cauzate de variabilele globale şi statice. Atâta timp cât variabilele (şi obiectele) pe care le foloseşte un fir de execuţie sunt create în stivă, problemele de reintrare nu mai apar deoarece fiecare fir de execuţie are stiva sa proprie.

Crearea unul fir de execuţie de lucru în MFC

Cea mai bună modalitate de a lansa un fir de execuţie într-o aplicaţie MFC este apelul AfxBeginThread. MFC defineşte 2 versiuni diferite de AfxBeginThread : una care porneşte un fir de execuţie cu interfaţă cu utilizatorul şi alta care porneşte un fir de execuţie de lucru. Codul sursă pentru ambele este găsit în Thrdcore.cpp. Nu trebuie să folosim funcţia Win32::CreateThread pentru a crea un fir de execuţie într-un program MFC decât doar dacă firul nu foloseşte MFC. AfxBeginThread nu este nici pe departe un wrapper pentru funcţia CreateThread ; în plus faţă de lansarea unui nou fir de execuţie, ea iniţializează informaţia internă de stare folosită de cadrul de lucru, efectuează verificări la diferite momente din timpul procesului de creare şi se asigură că funcţiile din biblioteca run-time C sunt accesate într-o modalitate sigura din punctul de vedere al firelor de execuţie.

AfxBeginThread facilitează crearea unui fir de execuţie de lucru. Atunci când este apelată, ea creează un nou obiect de tip CWinThread, lansează un fir de execuţie, îl ataşează obiectului CWinThread şi returnează un pointer CWinThread. Declaraţia CWinThread* pThread = AfxBeginThread (

ThreadFunc, &threadInfo); porneşte un fir de execuţie de lucru şi îi transmite structura de date predefinită de aplicaţie (&ThreadInfo) care conţine intrarea către firul de execuţie. ThreadFunc este funcţia de fir de execuţie – funcţia care este executată atunci când firul de execuţie începe să se execute. O funcţie de fir de execuţie foarte simplă care se învârte într-o buclă care „mănâncă” din procesor şi apoi se termină arată în felul următor :

Page 7: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

7

UINT ThreadFunc (LPVOID pParam) {

UINT nlterations = (UINT) pParam; for(UINT i=0; i<nlterations; i++);

return 0; }

În acest exemplu, valoarea transmisă în pParam nu este un pointer, ci un UINT normal. Funcţiile de fir de execuţie sunt descrise mai în detaliu în următoarea secţiune.

Funcţia AfxBeginThread acceptă patru parametri opţionali care specifică prioritatea firului, mărimea stivei, indicatorii de creere şi atributele de securitate. Prototipul complet al funcţiei este:

CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc,

LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES 1pSecurityAttrs = NULL)

nPriority defineşte prioritatea firului de execuţie. Fire de execuţie cu

prioritate mare sunt programate pentru procesor în faţa celor cu prioritate mai scăzută, dar în practică, chiar şi fire de execuţie cu prioritate foarte scăzută de obicei au tot timpul de procesor de care au nevoie. nPriority nu reprezintă un nivel absolut de prioritate. El precizează un nivel de prioritate relativ faţă de nivelul de prioritate al procesului caruia îi aparţine firul. Implicit prioritatea este THREAD_PRIORITY_NORMAL, care atribuie firului de execuţie aceeaşi prioritate ca şi a procesului care îl deţine. Nivelul de prioritate se poate schimba în orice moment cu comanda CWinThread::SetThreadPriority.

Parametrul nStackSize transmis către AfxBeginThread precizează mărimea maximă a stivei a firului. In mediul Win32, fiecare fir de execuţie primeşte stiva sa proprie. Valoarea 0 implicită a variabilei nStackSize permite stivei să atingă mărimea maximă de 1MB. Aceasta nu înseamnă ca fiecare fir de execuţie are nevoie de minim 1 MB de memorie; înseamnă ca fiecărui fir de execuţie i se alocă 1 MB din spaţiul de adrese din spaţiul de 4GB în care se execută aplicaţiile Windows pe 32 de biţi. Memoria nu este alocată spaţiului de adrese al stivei până când nu este necesar, aşa că majoritatea stivelor nu folosesc niciodată mai mult de câţiva KB din memoria fizică. Precizând o limită pentru mărimea stivei permite sistemului de operare să „prindă” funcţiile care se apelează recursiv la infinit şi care eventual consumă stiva. Limita standard de 1 MB este suficientă pentru aproape toate aplicaţiile.

dwCreateFlags poate fi doar una din două valori. Valoarea implicită 0 precizează sistemului să execute firul imediat. Dacă este specificat în schimb CREATE_SUSPENDED, firul porneşte în starea suspendată şi nu îşi începe execuţia până când alt fir de execuţie (de obicei firul care l-a creat ) apelează CWinThread::ResumeThread asupra firului suspendat, ca mai jos : CWinThread* pThread = AfxBeginThread (

ThreadFunc, SthreadInfo,

Page 8: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

8

THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);

//Porneşte firul de execuţie. pThread->ResumeThread();

Uneori este util să creăm un fir şi să îi amânăm execuţia până mai târziu.

Indicatorul CREATE_SUSPENDED este mecanismul prin care putem declara o execuţie cu întârziere.

Ultimul parametru din lista de argumente a funcţiei AfxBeginThread, IpSecurityAttrs, este un pointer către o structură SECURITY_ATTRIBUTES care conţine atributele de securitate ale firului de execuţie nou creat şi care de asemenea precizează sistemului dacă procesele copil ar trebui să moştenească handler-ul firului de execuţie. Valoarea implicită NULL atribuie noului fir de execuţie aceleaşi proprietăţi ca şi a firului care l-a creat.

Crearea unui fir de execuţie cu interfaţă cu utilizatorul în MFC

Crearea unui astfel de fir de execuţie este diferită de procesul de creare a unui fir de execuţie de lucru. Un fir de lucru este definit de funcţia sa iar comportarea unui fir de execuţie cu interfaţă este dată de o clasă care se poate crea dinamic şi care este derivată din CWinThread, asemănător unei clase de aplicaţie derivată din CWinApp. Clasa de mai jos creează o fereastră de cadru de tip top-level care se închide singură când este apăsat butonul stânga al mouse-ului. Închiderea ferestrei opreşte şi firul de execuţie deoarece funcţia CWnd::OnNcDestroy trimite un mesaj WM_QUIT către coada de mesaje a firului de execuţie. Trimiterea acestui mesaj WM_QUIT către un fir de execuţie principal opreşte firul de execuţie şi termină aplicaţia.

// Clasa CUIThread class CUIThread : public CWinThread {

DECLARE_DYNCREATE (CUIThread) public: virtual BOOL Initlnstance (); IMPLEMENT_DYNCREATE (CUIThread, CWinThread)

}; BOOL CUIThread::Initlnstance () {

m_pMainWnd = new CMainWindow m_pMainWnd->ShowWindow (SW_SHOW); m_pMainWnd->UpdateWindow (); return TRUE;

} // Clasa CMainWindow class CMainWindow : public CFrameWnd { public: CMainWindow (); protected:

Page 9: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

9

afx_msg void OnLButtonDown (UINT, CPoint); // Se declară harta de mesaje DECLARE_MESSAGE_MAP () }; BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)

ON_WM_LBUTTONDOWN () END_MESSAGE_MAP () CMainWindow::CMainWindow () {

Create (NULL, _T ("UI Thread Window")); } void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) {

PostMessage (WM_CLOSE, 0, 0); }

Trebuie remarcat parametrul SW_SHOW transmis către ShowWindow în locul parametrului normal m_nCmdShow. Acesta este un membru al CWinApp, aşa că atunci când creăm o fereastră top-level de la un fir de execuţie cu interfaţă este de datoria noastră să precizăm starea iniţială a ferestrei.

Putem lansa un CUIThread apelând o formă a funcţiei AfxBeginThread care acceptă un pointer CRuntimeClass către clasa firului de execuţie : CWinThread* pThread = AfxBeginThread(

RUNTIME_CLASS (CUIThread)); Versiunea funcţiei AfxBeginThread pentru fire de execuţie cu interfaţă

acceptă aceiaşi patru parametri opţionali ca şi versiunea pentru firele de lucru, dar nu acceptă o valoare pParam. Odată pornit, un fir de execuţie cu interfaţă rulează asincron respectând firul care l-a creat.

Suspendarea şi repornirea firelor de execuţie

Un fir de execuţie în MFC care rulează poate fi suspendat prin apelul CWinThread::SuspendThread şi poate si repornit cu CWinThread::ResumeThread. Un fir de execuţie poate apela funcţia SuspendThread pe el însuşi sau un alt fir poate face acest apel pentru el. Totuşi, un fir suspendat nu poate apela ResumeThread pentru a se reporni; altcineva trebuie să facă acest apel. Un fir de execuţie suspendat nu consumă aproape deloc din timpul procesorului şi impune în sistem o suprasarcină practic nulă.

Pentru fiecare fir de execuţie Windows-ul păstrează un contor de suspendare care este incrementat de SuspendThread şi decrementat de ResumeThread. Un fir de execuţie este programat pentru timpul procesorului doar atunci când contorul său de suspendare este 0. Dacă funcţia SuspendThread este apelată de două ori succesiv, ResumeThread trebuie apelată de asemenea de două ori. Un fir de execuţie creat fără atributul CREATE_SUSPENDED are iniţial contorul de

Page 10: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

10

suspendare egal cu 0. Un fir creat cu atributul CREATE_SUSPENDED se porneşte având iniţial contorul de suspendare iniţializat cu valoarea 1. Atât SuspendThread cât şi ResumeThread returnează contorul de suspendare anterior, aşa că pentru a fi siguri că un fir de execuţie va fi repornit indiferent de cat este de mare valoarea contorului de suspendare trebuie să apelăm ResumeThread în mod repetat până când returnează valoarea 1. ResumeThread returnează 0 dacă firul nu este apelat sau dacă nu este suspendat.

În viaţa reală, o aplicaţie trebuie să fie atentă atunci când apelează SuspendThread deoarece nu avem nici o idee despre ce poate acesta să facă atunci când încercăm să îi reluam execuţia. Dacă firul încearcă să aloce memorie dintr-un heap, de exemplu, firul va trebui să se blocheze pe heap. Pe măsură ce alte fire încearcă să acceseze heap-ul, execuţia lor va fi oprită până când primul fir îşi reia execuţia. SuspendThread este neprimejdios doar atunci când ştim ce este exact firul ţintă (sau ce face) şi luăm masuri extreme pentru a evita problemele sau blocajele cauzate de suspendarea firului.

În SDK, un fir de execuţie poate fi suspendat prin apelul funcţiei SuspendThread şi apoi îi putem relua execuţia cu ajutorul funcţiei ResumeThread. Ambele funcţii sunt asemănătoare cu funcţiile similare din MFC.

Punerea unui fir de execuţie în starea de „sleep”

Un fir de execuţie poate să fie suspendat pentru o anumită perioadă de timp prin apelul funcţiei API ::Sleep. Un fir suspendat nu consumă din timpul procesorului. Declaraţia ::Sleep (10000) suspendă firul de execuţie curent pentru 10 secunde.

O utilizare pentru ::Sleep este pentru a implementa fire de execuţie ale căror acţiuni sunt înnăscut bazate pe timp, ca de exemplu firul de execuţie din fundalul unui control de animaţie sau un fir care muta acele unui ceas. ::Sleep poate fi folosită şi pentru a elibera resturile unor alte perioade de timp ale firului de execuţie. Declaraţia ::Sleep(0) suspendă firul curent şi permite planificatorului să ruleze alte fire de aceeaşi prioritate sau de prioritate mai ridicată. Dacă nici unul din astfel de fire nu aşteaptă execuţia, apelul funcţiei returnează imediat şi firul curent îşi continua execuţia. În Windows NT 4.0 sau mai sus se poate comuta pe alt fir de execuţie prin apelul funcţiei SwitchToThread. Vom folosi ::Sleep(0) dacă codul pe care îl scriem trebuie să meargă pe toate platformele Win32.

Dacă scriem o aplicaţie care foloseşte mai multe fire de execuţie pentru a desena pe ecran, câteva declaraţii ::Sleep(0) plasate strategic pot face adevărate minuni pentru calitatea ieşirii. Să presupunem că realizăm o animaţie cu mişcarea a patru obiecte şi atribuim fiecărui obiect firul său propriu de execuţie. Fiecare fir este responsabil cu mişcarea unui obiect pe ecran. Dacă doar rulăm fiecare fir într-o buclă şi îi dăm posibilitatea să acapareze tot timpul de procesor pe care îl poate lua, este foarte posibil ca mişcarea obiectelor să fie neregulată. Dar dacă lăsăm fiecare fir să îşi mute obiectul câţiva pixeli şi apoi apelăm ::Sleep(0), animaţia va fi mult mai lină.

Valoarea pe care o transmitem funcţiei ::Sleep nu garantează că firul va fi repornit exact la momentul terminării intervalului de timp. Transmiterea către ::Sleep a valorii 10000 ne garantează că firul va fi repornit după cel puţin 10 secunde. Firul va putea să rămână suspendat 10 secunde, sau chiar 20, acest lucru fiind în funcţie de sistemul de operare. În practică, firul de obicei va fi repornit cu o fracţiune de secundă după terminarea intervalului de timp, dar nu există nici o garanţie. La momentul actual nu există în nici o versiune de Windows o metodă de a suspenda un fir de execuţie pentru o perioadă precisă de timp.

Page 11: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

11

Schimbarea către un alt fir de execuţie

Sistemul oferă o funcţie numită SwitchToThread care permite rularea unui alt fir de execuţie dacă există :

BOOL SwitchToThread ();

Atunci când apelăm această funcţie, sistemul verifică dacă există un fir care este privat de timp de procesor. Dacă nu este găsit nici un astfel de fir, funcţia SwitchToThread returnează imediat. Dacă există un astfel de fir, funcţia planifică firul (care este posibil să aibă o prioritate mai scăzută decât firul care a apelat funcţia SwitchToThread). Acestui fir îi este permis să ruleze pentru o cuantă de timp iar după aceea planificatorul funcţionează normal.

Această funcţie permite unui fir de execuţie care doreşte accesul la o resursă să forţeze un fir cu o prioritate mai mică care deţine acea resursă să elibereze resursa. Dacă nici un fir nu poate fi executat după apelul SwitchToThread, funcţia returnează FALSE; în caz contrar, ea returnează o valoare nenulă.

Apelul SwitchToThread este similar cu apelul funcţiei Sleep cu parametrul având valoarea 0. Diferenţa este că SwitchToThread permite execuţia firelor de execuţie cu prioritate mai mică. Sleep replanifică firul apelant chiar dacă există fire cu prioritate mai redusă care sunt private de timp de procesor.

Windows 98 nu are o implementare folositoare pentru această funcţie.

Stiva unui proces

Fiecărui proces îi este alocat propria sa stivă din spaţiul de 4 GB de memorie al procesului părinte. Atunci când folosim variabile statice sau globale, firele multiple de execuţie pot accesa variabilele în acelaşi timp, putând să corupă conţinutul variabilelor. Totuşi, variabilele locale şi automate sunt create în stiva firului de execuţie şi din această cauză sunt mai puţin probabil să fie corupte de un alt fir de execuţie. Din acest motiv, trebuie întotdeauna să încercăm să folosim variabile locale sau automate atunci când scriem funcţiile noastre şi să evităm folosirea variabilelor statice şi globale.

Terminarea unui fir de execuţie în SDK

Asemenea unui proces, un fir poate fi oprit în trei feluri : 1. Firul se opreşte singur apelând funcţia ExitThread. (cea mai întâlnită

metodă) 2. Un fir din acelaşi proces sau din alt proces apelează TerminateThread.

(de evitat) 3. Procesul care conţine firul se opreşte.

Funcţia ExitThread

Un fir se opreşte atunci când apelează ExitThread :

VOID ExitThread (UINT fuExitCode);

Această funcţie opreşte firul şi setează codul de ieşire al firului la fuExitCode. Funcţia ExitThread nu returnează o valoare deoarece firul nu s-a oprit.

Această metodă este cea mai întâlnită deoarece ExitThread este apelat atunci când funcţia de fir returnează către funcţia internă a sistemului StartOfThread. Aceasta apelează la rândul ei ExitThread, transmiţându-i valoarea returnată din funcţia noastră de fir.

Page 12: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

12

Funcţia TerminateThread

Un apel TerminateThread termină de asemenea firul :

BOOL TerminateThread ( HANDLE hThread, DWORD dwExitCode);

Funcţia setează firul identificat de parametrul hThread şi setează codul de

ieşire la dwExitCode. Funcţia TerminateThread există astfel încât să putem să oprim un fir atunci când nu mai răspunde. Este recomandat să o folosim doar ca ultim mijloc.

Atunci când un fir „moare” prin apelul ExitThread, stiva firului este distrusă. Totuşi, dacă firul este oprit prin TerminateThread, sistemul nu distruge stiva până când procesul care este proprietarul firului se termină, deoarece alte fire de execuţie s-ar putea să folosească pointeri care referenţiază date conţinute pe stiva firului care s-a oprit. Dacă aceste alte fire încearcă să acceseze stiva, apare o violare de acces.

Atunci când firul se termină, sistemul anunţă orice DLL-uri ataşate procesului proprietar al firului că firul se opreşte. Dacă apelăm însă TerminateThread, sistemul nu anunţă nici un DLL ataşat procesului, adică procesul nu va fi închis corect. De exemplu, un DLL poate fi scris pentru a goli un buffer într-un fişier pe disc atunci când firul se detaşează de DLL. Deoarece DLL-ul nu este anunţat de sistem de detaşare atunci când folosim TerminateThread, DLL-ul nu poate efectua operaţia sa normală de curăţire.

Oprirea unui fir de execuţie în MFC

Odată ce un fir de execuţie este pornit, el poate fi oprit în două moduri. Un fir de execuţie de lucru se opreşte atunci când funcţia de fir execută o declaraţie return sau când orice altă funcţie oriunde în fir apelează AfxEndThread. Un fir de execuţie cu interfaţă este oprit atunci când este trimis un mesaj WM_QUIT către coada de mesaje sau când firul însuşi apelează AfxEndThread. Un fir de execuţie poate adăuga un mesaj WM_QUIT la el însuşi cu funcţia API ::PostQuitMessage. AfxEndThread, ::PostQuitMessage şi return acceptă toate un cod de ieşire pe 32 de biţi care poate fi extras cu ::GetExitCodeThread după oprirea firului de execuţie. Următoarea declaraţie copie codul de ieşire al firului indicat de pThread în dwExitCode:

DWORD dwExitCode; ::GetExitCodeThread (pThread->m_hThread, &dwExitCode);

Dacă funcţia ::GetExitCodeThread este apelată pentru un fir care este în

execuţie , atunci atribuie variabilei dwExitCodeThread valoarea STILL_ACTIVE(0x103). În acest exemplu, identificatorul firului transmis către funcţia ::GetExitCodeThread este extras din data membru m_hThread a obiectului CWinThread care încapsulează firul de execuţie. Ori de câte ori avem un CWinThread şi vrem să apelăm o funcţie API care are nevoie de un handle de fir, putem obţine acest handle din m_hThread.

Page 13: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

13

Ştergerea automată a CWinThread-urilor

Mostra de cod din secţiunea precedentă pare suficient de nevinovată, dar de fapt este un accident pe cale să se întâmple dacă nu suntem conştienţi de caracteristica specială a CWinThread şi nu facem nimic să luăm în seamă acest lucru.

Ştim că funcţia AfxBeginThread creează un obiect CWinThread şi returnează adresa către apelant. Dar cum este şters acel obiect CWinThread ? Pentru a nu fi nevoie să apelăm noi delete pe pointerul CWinThread returnat de AfxBeginThread, MFC-ul apelează el delete asupra pointerului după ce firul de execuţie a fost oprit. Mai mult, destructorul CWinThread utilizează funcţia API ::CloseHandle pentru a închide handler-ul firului. Handler-ele de fire de execuţie trebuie închise în mod expres deoarece ele rămân deschise chiar şi după ce firele de execuţie asociate cu ele au fost oprite. Handler-ele trebuie să rămână deschise; altfel, funcţii ca ::GetExitCodeThread nu ar putea să funcţioneze.

În aparenţă, faptul că MFC-ul şterge în mod automat obiectele CWinThread şi închide handler-ele de fire care corespund firelor pare convenabil. Dacă MFC-ul nu ar fi avut grijă de aceste lucruri pentru noi, am fi trebuit să le facem noi înşine. Dar există o problemă, sau cel puţin una potenţială. Să analizăm din nou declaraţia :

::GetExitCodeThread (pThread->m_nThread, &dwExitCode);

Totul este în ordine cu acest cod dacă firul de execuţie nu este terminat, deoarece pThread este încă un pointer valid. Dar dacă firul este terminat, este foarte posibil ca MFC-ul să fi şters obiectul CWinThread si acel pThread este acum un pointer invalid. (Am precizat „foarte posibil” deoarece o scurtă perioadă de timp desparte terminarea firului şi ştergerea obiectului CWinThread asociat lui.) O soluţie evidentă este de a copia handler-ul firului din obiectul CWinThread într-o variabilă locală înainte de oprirea firului de execuţie şi de a folosi acel handle în apelul ::GetExitCodeThread:

// În timp ce firul rulează. HANDLE hThread = pThread->m_hThread; // Cândva mai târziu. ::GetExitCodeThread(hThread, &dwExitCode);

Dar si acest cod poate prezenta probleme. De ce ? Deoarece dacă obiectul

CWinThread nu mai există, handler-ul firului nu mai există nici el; a fost închis de mult timp. Greşeala de a lua în calcul ştergerea automată a obiectelor CWinThread şi apelul ::CloseHandle executat de destructorul CWinThread poate conduce la mari greşeli de programare dacă folosim funcţii ca ::GetExitCodeThread care presupun că handler-ul firului este încă valid chiar dacă firul nu mai rulează.

Din fericire, această problemă are o soluţie, de fapt două. Prima soluţie este de a împiedica MFC-ul să şteargă obiectul CWinThread setând data membru m_bAutoDelete cu valoarea FALSE. Valoarea implicită este TRUE, care permite ştergerea automată. Dacă alegem această variantă, trebuie să apelăm delete asupra pointerului CWinThread returnat de AfxBeginThread, altfel aplicaţia va suferi scurgeri de memorie. Următorul fragment de cod ilustrează acest lucru:

CWinThread* pThread = AfxBeginThread ( ThreadFunc, NULL,

THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);

Page 14: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

14

pThread->m_bAutoDelete = FALSE; pThread->ResumeThread(); // Cândva mai târziu. DWORD dwExitCode; ::GetExitCodeThread (pThread->m_hThread, &dwExitCode); if (dwExitCode == STILL_ACTIVE) { // Firul încă rulează. } else { // Firul şi-a terminat execuţia. Ştergem obiectul CWinThread. delete pThread; }

La fel de important ca ştergerea obiectului CWinThread este crearea unui fir în stare suspendată. Dacă nu procedăm astfel, există o şansă mica dar foarte reală ca noul fir de execuţie să îşi termine existenţa înainte ca firul care l-a creat să execute instrucţiunea care setează m_bAutoDelete la valoarea FALSE. Trebuie să ne aducem aminte că o dată ce un fir de execuţie este pornit, Windows-ul nu oferă nici o garanţie despre cât timp de procesor va fi acordat acelui fir. Cea de-a doua soluţie este să permitem obiectului CWinThread să fie şters automat, dar să folosim funcţia Win32 ::DuplicateHandle pentru a crea o copie a handler-ului firului. Handler-urile firelor au un contor de referinţă şi folosirea funcţiei ::DuplicateHandle pentru a duplica un fir proaspăt deschis incrementează contorul de referinţă de la 1 la 2. În consecinţă, atunci când destructorul CWinThread apelează ::CloseHandle, handler-ul nu este de fapt închis; el are pur şi simplu contorul de referinţă decrementat. Partea negativă este că nu trebuie să uităm să apelăm ::CloseHandle pentru a închide handler-ul. Un exemplu:

CWinThread* pThread = AfxBeginThread( ThreadFunc, NULL,

THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); HANDLE hThread; ::DuplicateHandle ( GetCurrentProcess(), pThread->m_nThread,

GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);

pThread->ResumeThread(); // Cândva mai târziu. DWORD dwExitCode; ::GetExitCodeThread(hThread, &dwExitCode); if (dwExitCode == STILL_ACTIVE) { // Firul încă rulează. } else { // Firul şi-a terminat execuţia. Închidem identificatorul firului.

Page 15: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

15

::CloseHandle(hThread); }

Din nou, firul de execuţie este creat într-o stare de suspendare astfel încât firul care l-a creat să poată să fie absolut sigur de execuţia codului înainte de terminarea noului fir de execuţie.

Vorbind la modul general, firele de execuţie se pot opri doar pe ele însele. Dacă dorim ca firul A să oprească firul B, trebuie să punem la punct un mecanism de semnalizare care permite firului A să spună firului B să se oprească singur. O variabilă simplă poate servi ca un atribut de cerere a terminării, aşa cum este demonstrat mai jos:

//Thread A nContinue = 1; CWinThread* pThread = AfxbeginThread( ThreadFunc, &nContinue); nContinue = 0; // Spunem firului B să îşi termine execuţia. //Thread B UINT ThreadFunc( LPVOID pParam) { int* pContinue = (int*)pParam; while(*pContinue) { // Diferite operaţii... } return 0; }

În acest exemplu, firul B verifică din timp în timp şi se opreşte dacă nContinue se schimbă de la o valoare diferită de 0 la 0. În mod normal nu este o idee prea bună ca două fire de execuţie să acceseze aceeaşi variabilă fără să îşi sincronizeze acţiunile, dar în acest caz este acceptabil deoarece firul B verifică doar dacă nContinue este 0. Bineînţeles, pentru a previne violările de acces trebuie să ne asigurăm că nContinue nu iese din raza de definire atât timp cât firul B rulează. Putem face acest lucru fie declarând variabila nContinue ca statică sau globală.

Să presupunem că vrem modificăm acest exemplu în aşa fel încât când firul A setează nContinue la 0 să facă o pauză până când firul B încetează să mai ruleze. Modul corect de a face acest lucru este următorul:

//Firul A nContinue = 1; CWinThread* pThread = AfxBeginThread(ThreadFunc, &nContinue); HANDLE hThread = pThread->m_nThread; // Salvează identificatorul firului nContinue = 0; // Spune firului B să se oprească. ::WaitForSingleObject(hThread, INFINITE); //Firul B

Page 16: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

16

UINT ThreadFunc (LPVOID pParam) {

int* pContinue = (int*) pParam; while (*pContinue) {

// efectuează diferite operaţii } return 0;

} ::WaitForSingleObject blochează firul de execuţie apelant până când

obiectul specificat – în acest caz un alt fir – intră într-o stare „semnalată”. Un fir devine semnalat când se termină. Când un fir de execuţie se blochează în ::WaitForSingleObject, el aşteaptă foarte eficient deoarece este efectiv suspendat până când apelul funcţiei returnează. Acest exemplu presupune că firul B nu se va opri până când firul A nu îi va spune acest lucru. În caz contrar ,dacă firul B se poate opri înaintea ca firul A să îi ceară acest lucru, atunci firul A ar trebui să creeze firul B intr-o stare se suspendare şi ar trebui să facă o copie a identificatorului firului cu ::DuplicateHandle. În caz contrar, firul A ar putea fi prins în greşeala transmiterii unui identificator invalid de fir către WaitForSingleObject.

::WaitForSingleObject este o funcţie indispensabilă pe care o vom folosi foarte des atunci când vom scrie cod cu multiplicare de fire. Primul parametru transmis către aceasta este identificatorul obiectului pe care vrem să îl aşteptăm. (Poate fi de asemenea un identificator de proces, identificatorul unui obiect de sincronizare sau identificatorul unei declaraţii de schimbare a unui fişier, printre alte lucruri.) În exemplul precedent, firul A extrage identificatorul firului B înainte de a seta nContinue cu 0 deoarece obiectul CWinThread reprezentând firul B s-ar putea să nu mai existe când se execută apelul ::WaitForSingleObject. Al doilea parametru al funcţiei ::WaitForSingleObject este lungimea timpului pe care suntem dispuşi să aşteptăm. INFINITE înseamnă atât de mult cât trebuie. Atunci când precizăm INFINITE, ne asumăm riscul ca firul apelant să poată să se blocheze dacă obiectul pe care îl aşteaptă nu devine niciodată semnalat. Dacă precizăm în schimb un număr de milisecunde, ca în

::WaitForSingleObject(hThread, 5000);

atunci ::WaitForSingleObject va returna după ce timpul specificat, în cazul de faţă 5 secunde, se va scurge chiar dacă obiectul nu a devenit semnalat. Putem verifica valoarea returnată pentru a verifica de ce a returnat funcţia. WAIT_OBJECT_0 înseamnă că obiectul a fost semnalat şi WAIT_TIMEOUT înseamnă că nu este.

Fiind dat un identificator de fir sau un obiect CWinThread valid care încapsulează un identificator de fir de execuţie, putem determina rapid dacă firul încă rulează apelând ::WaitForSingleObject si specificând 0 pentru perioada de expirare, ca mai jos:

if (::WaitForSingleObject (hThread, 0) == WAIT_OBJECT_0) { //Firul de execuţie nu mai există. } else {

Page 17: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

17

//Firul de execuţie încă rulează. }

Apelată în acest mod, ::WaitForSingleObject nu mai aşteaptă; ea returnează imediat. O valoare de retur egală cu WAIT_OBJECT_0 înseamnă ca firul este semnalat (nu mai există), iar o valoare egală cu WAIT_TIMEOUT înseamnă ca firul nu este semnalat (încă mai există). Ca de obicei, rămâne la latitudinea noastră să ne asigurăm că identificatorul pe care îl transmitem către ::WaitForSingleObject este unul valid, fie duplicând identificatorul firului original, fie prevenind ştergerea automată a obiectului CWinThread.

Există o modalitate prin care un fir poate termina direct execuţia unui alt fir, dar această metodă trebuie folosită doar ca o ultimă opţiune. Declaraţia :

::TerminateThread(hThread, 0);

opreşte firul de execuţie a cărui identificator este hThread şi îi atribuie codul de ieşire 0. Referirea API Win32 documentează o parte din multele probleme pe care :.TerminateThread le poate cauza, care variază de la obiecte de sincronizare care rămân fără firele de execuţie aferente până la DLL-uri care nu mai apucă să ruleze un cod normal de închidere a firelor de execuţie.

Ce se întâmplă atunci când procesul se opreşte

Funcţiile ExitProcess sau TerminateProcess discutate anterior termină de asemenea execuţia firelor. Diferenţa este că aceste funcţii opresc toate firele conţinute în procesul care se termină.

Ce se întâmplă la oprirea unui fir de execuţie

Următoarele acţiuni au loc la oprirea unui fir de execuţie : 1. Toţi identificatorii obiectelor utilizator aparţinând firului sunt eliberaţi.

În Win32, majoritatea obiectelor sunt proprietatea procesului care conţine firul care a creat obiectele. Totuşi, două obiecte utilizator pot fi proprietatea unui fir de execuţie : ferestre şi legături. Atunci când firul care a creat aceste obiecte îşi termină execuţia, sistemul distruge în mod automat obiectele. Celelalte obiecte sunt distruse doar atunci când procesul proprietar se termină.

2. Starea obiectului nucleu fir de execuţie devine semnalată. 3. Codul de ieşire al firului se schimbă de la STILL_ACTIVE la codul de

terminare al firului. 4. Dacă firul este ultimul fir activ din proces, procesul se opreşte. 5. Contorul de utilizare al obiectului nucleu fir este decrementat cu 1. Atunci când firul se termină, obiectul nucleu fir asociat lui nu devine

automat liber până când toate referinţele rămase la obiect nu sunt închise. De îndată de un fir nu mai rulează, orice alt fir din sistem nu mai are ce face

cu identificatorul primului gir. Totuşi, aceste alte fire pot apela GetExitCodeThread pentru a verifica dacă firul identificat prin hThread s-a terminat şi, în caz afirmativ, să îi determine codul său de ieşire.

BOOL GetExitCodeThread (HANDLE hThread, LPDWORD lpdwExitCode);

Valoarea de ieşire este returnată în DWORD-ul la care pointează lpdwExitCode. Dacă firul nu este terminat atunci când este apelat GetExitCodeThread, funcţia scrie în DWORD identificatorul STILL_ACTIVE ( definit ca 0x103). Dacă funcţia reuşeşte este returnat TRUE.

Page 18: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

18

Fire de execuţie şi sincronizarea firelor de execuţie

În mediul Microsoft Win32, fiecare aplicaţie rulată reprezintă un proces şi fiecare proces conţine unul sau mai multe fire de execuţie. Un fir de execuţie este o cale de execuţie prin codul unui program şi în plus un set de resurse (stive, registre de stare şi aşa mai departe) atribuite de către sistemul de operare.

O diferenţă fundamentală dintre versiunile Microsoft Windows pe 16 biţi si pe 32 de biţi este aceea că Windows-ul pe 32 de biţi nu îşi limitează aplicaţiile la un singur fir de execuţie fiecare. Un proces într-o aplicaţie Windows 32 îşi începe viaţa ca un fir de execuţie unic, dar acel fir de execuţie poate genera alte fire de execuţie. Un planificator preliminar in nucleul sistemului de operare împarte timpul procesorului între firele de execuţie active astfel încât ele par să ruleze simultan. Firele de execuţie sunt ideale pentru efectuarea unor operaţii in fundal în timp ce se procesează intrarea de la utilizator în prim plan. Ele pot de asemenea juca roluri mai vizibile creând ferestre şi procesând mesajele către aceste ferestre, în acelaşi mod în care firul principal de execuţie procesează mesajele trimise către o fereastra principală a unei aplicaţii.

Multiplicarea firelor de execuţie nu este la îndemâna oricui. Aplicaţiile cu mai multe fire de execuţie sunt dificil de scris şi de depanat din cauză că paralelismul firelor de execuţie concurente adaugă un nivel suplimentar de complexitate codului programului. Folosite corect insă, firele de execuţie multiple pot să îmbunătăţească dramatic timpul de răspuns al unei aplicaţii. Un procesor de cuvinte care face verificarea de sintaxă într-un fir de execuţie dedicat, de exemplu, poate continua să proceseze mesajele în firul principal de execuţie şi îi dă posibilitatea utilizatorului să continue să lucreze în timp ce verificarea de sintaxă îşi continua rularea. Ce face mai dificil să scriem un procesor de text cu mai multe fire de execuţie este faptul că firul de execuţie care efectuează verificarea de sintaxă va trebui in mod inevitabil să îşi sincronizeze acţiunile cu alte fire de execuţie din cadrul aplicaţiei. Majoritatea programatorilor au fost condiţionaţi să gândească în termeni sincroni la codul lor : funcţia A apelează funcţia B, funcţia B efectuează o operaţie şi returnează în A şi aşa mai departe. Dar firele de execuţie sunt asincrone prin natura lor. Într-o aplicaţie cu mai multe fire de execuţie, trebuie să ne gândim la ce se întâmplă dacă, de exemplu, două fire de execuţie apelează funcţia B în acelaşi moment sau un fir de execuţie citeşte o variabilă în timp ce celălalt o scrie. Dacă funcţia A lansează funcţia B într-un fir de execuţie separat, trebuie să anticipăm de asemenea problemele care ar putea să apară dacă funcţia A continuă să ruleze in timpul execuţiei funcţiei B. De exemplu, este uzual să transmitem adresa unei variabile creată în stivă în funcţia A către funcţia B pentru procesare. Dar funcţia B este într-un alt fir de execuţie, variabila s-ar putea să nu mai existe atunci când funcţia B ajunge să o proceseze. Chiar şi cel mai inofensiv cod la prima vedere poate fi corupt în mod fatal atunci când implică două fire de execuţie diferite.

Fire de execuţie, procese şi priorităţi

Planificatorul este acea componentă a sistemului de operare care decide care fire de execuţie rulează, când şi pentru cât timp. Planificarea firelor de execuţie este o sarcină complexă al cărei obiectiv este de a împărţi timpul procesorului între firele de execuţie cât mai eficient cu putinţă pentru a crea iluzia că toate rulează în acelaşi timp. Pe maşinile cu mai multe procesoare, Windows NT şi Windows 2000 rulează în realitate două sau mai multe fire de execuţie în acelaşi timp distribuind

Page 19: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

19

firele între procesoare folosind o schemă numită procesare multiplă simetrică, sau SMP. Windows 95 şi Windows 98 nu sunt sisteme de operare SMP, astfel încât ele distribuie toate firele aceluiaşi procesor chiar şi pe calculatoarele cu mai multe procesoare.

Planificatorul foloseşte o varietate de tehnici pentru a îmbunătăţi performanţa procesării multiple şi pentru a încerca să asigure faptul că fiecare fir de execuţie primeşte suficient timp de procesor. În cele din urmă, totuşi, decizia care fir să se execute mai întâi este dată de prioritatea firelor. La orice moment dat, fiecare fir are atribuită un nivel de prioritate de la 0 la 31, cu un număr mai mare indicând o prioritate mai mare. Dacă un fir cu prioritatea 11 aşteaptă execuţia şi toate celelalte fire care aşteaptă execuţia au priorităţi mai mici decât 11, firul cu prioritatea 11 rulează primul. Dacă două fire au aceeaşi prioritate, planificatorul îl execută pe cel care a fost executat mai de mult timp. Atunci când perioada de timp, sau cuantum, acordată firului expiră, celălalt fir cu prioritate egală cu primul este executat dacă toate celelalte fire active au priorităţi mai mici. Ca o regulă generală, planificatorul întotdeauna acordă următoarea perioadă de timp firului în aşteptare cu cea mai mare perioadă.

Acest lucru înseamnă că firele cu prioritate scăzută nu sunt executate niciodată ? Nici gând. În primul rând, să ne aducem aminte că Windows-ul este un sistem de operare bazat pe masaje. Daca un fir de execuţie apelează ::GetMessage şi coada sa de mesaje este goală, firul se blochează până când devine disponibil un mesaj. Acest lucru dă şansa firelor cu prioritate redusă de a fi executate. Majoritatea firelor cu interfaţă îşi petrec mare parte din timpul lor blocate pe coada de mesaje, aşa că atât timp cât un fir de lucru cu prioritate ridicată nu monopolizează procesorul, chiar şi firele cu prioritate redusă practic beneficiază de tot timpul de procesor de care au nevoie. (Un fir de execuţie de lucru nu se blochează pe coada de mesaje deoarece nu procesează mesaje.)

Planificatorul face mai multe trucuri cu nivelele de prioritate pentru a îmbunătăţi disponibilitatea sistemului de răspuns şi pentru a reduce pericolul ca un fir oarecare să nu primească timp de procesor. Dacă un fir cu prioritatea 7 stă mai mult timp fără să primească timp de procesor, planificatorul poate mari temporar prioritatea firului la 8, 9 sau chiar mai mare pentru a-i da şansa de a se executa. Windows NT 3.x creşte priorităţile firelor care se execută în prim-plan pentru a îmbunătăţi timpul de răspuns al aplicaţiei în care lucrează utilizatorul, iar Windows NT 4.0 Workstation creşte perioada de timp acordată acestor fire. Windows-ul foloseşte o tehnică numită moştenirea priorităţii pentru a preveni blocarea pentru prea mult timp a firelor cu prioritate ridicată pe obiecte de sincronizare deţinute de fire cu prioritate scăzută. De exemplu, dacă un fir de execuţie cu prioritatea 11 încearcă să revendice un mutex deţinut de un fir cu prioritatea 4, planificatorul poate să mărească prioritatea celui de-al doilea fir pentru ca mutex-ul să se elibereze mai devreme.

De fapt cum sunt atribuite priorităţile prima dată ? Atunci când apelăm AfxBeginThread sau CWinThread::SetThreadPriority noi specificăm prioritatea relativă a firului. Sistemul de operare combină nivelul de prioritate relativ cu clasa de prioritate a procesului tată al firului pentru a calcula un nivel de prioritate de bază pentru fir. Nivelul real de prioritate al firului, un număr între 0 şi 31, poate varia continuu deoarece prioritatea poate creşte şi scădea. Nu putem controla creşterea (şi nici nu am vrea să facem acest lucru chiar dacă am putea să îl facem), dar putem să controlăm nivelul de prioritate de bază setând clasa de prioritate a procesului şi nivelul de prioritate relativ al firului.

Page 20: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

20

Prioritatea proceselor şi a firelor de execuţie

Clasele de prioritate ale proceselor

Majoritatea proceselor îşi încep existenţa cu clasa de prioritate NORMAL_PRIORITY_CLASS. O dată pornite însă, un proces să îşi schimbe prioritatea apelând ::SetPriorityClass, care acceptă ca argumente un identificator de proces (care poate fi obţinut cu apelul ::GetCurrentProcess) şi unul din specificatorii din tabelul următor :

Clasele de prioritate ale proceselor

Clasa de prioritate Descriere

IDLE_PRIORITY_CLASS Procesul rulează doar când sistemul este liber, de exemplu când nici un alt fie de execuţie nu aşteaptă să fie executat.

NORMAL_PRIORITY_CLASS Clasa de prioritate implicită. Procesul nu are nevoi speciale de planificare.

HIGH_PRIORITY_CLASS Procesul primeşte o prioritate mai mare ca un proces IDLE_PRIORITY_CLASS sau NORMAL_PRIORITY_CLASS.

REALTIME_PRIORITY_CLASS Procesul are cea mai mare prioritate posibilă, mai ridicată chiar decât HIGH_PRIORITY_CLASS.

Majoritatea aplicaţiilor nu au nevoie să îşi schimbe clasa de prioritate. Procesele cu priorităţile HIGH_PRIORITY_CLASS sau REALTIME_PRIORITY_CLASS pot afecta timpul de răspuns al sistemului şi pot chiar întârzia activităţi sistem vitale, cum ar golirea zonei de cache a harddiskului. O folosire corectă a clasei HIGH_PRIORITY_CLASS este pentru aplicaţiile sistem care rămân ascunse majoritatea timpului dar produc o fereastră atunci când are loc un eveniment de intrare. Aceste aplicaţii impun o sarcină foarte mică asupra sistemului atât timp cât sunt blocate aşteptând o intrare, dar o dată ce apare o intrare ele primesc o prioritate mai mare decât restul aplicaţiilor. Clasa REALTIME_PRIORITY_CLASS este furnizată în primul rând pentru programele care primesc date în mod real şi care trebuie să aibă parte de partea leului din timpul procesorului pentru a funcţiona corect. Clasa IDLE_PRIORITY_CLASS este ideală pentru protecţia ecranului, monitoare de sistem sau alte aplicaţii cu prioritate redusă care sunt proiectate să opereze neobstructiv în fundal.

Algoritmul de planificare are un efect important asupra tipurilor de aplicaţii pe care le pot rula utilizatorii. Încă de la început, dezvoltatorii de la Microsoft şi-au dat seama că vor trebui să modifice algoritmul de planificare în timp pe măsură ce scopul calculatoarelor se va schimba. Dar dezvoltatorii de software au nevoie să scrie programe acum şi Microsoft garantează că programele vor rula şi în versiuni ulterioare ale sistemului. Cum poate Microsoft să schimbe modulde funcţionare al sistemului şi totuşi să păstreze softul în stare de funcţionare ? Iată câteva răpunsuri :

• Microsoft nu oferă documentaţia completă a planificatorului; • Microsoft nu permite aplicaţiilor să beneficieze de toate avantajele

planificatorului; • Microsoft ne spune că algoritmul de planificare poate fi schimbat astfel

incât noi programăm defensiv.

Page 21: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

API-ul Windows oferă un strat abstract pentru planificatorul sistemului, astfel încât nu avem acces direct la planificator. În schimb, noi apelăm funcţii Windows care interpretează parametrii noştri în funcţie de versiunea sistemului de operare pe care rulăm.

Atunci când proiectăm o aplicaţie, trebuie să ne gândim la alte aplicaţii pe care utilizatorul le va rula împreună cu aplicaţia noastră. Apoi va trebui să alegem o clasă de prioritate bazată pe viteza de răspuns pe care dorim să o aibă firele de execuţie din această aplicaţie.

O dată ce alegem o clasă de prioritate, nu trebuie să ne mai gândim cum interacţionează aplicaţia noastră cu alte aplicaţii si trebuie să ne concentrăm asupra firelor din aplicaţie. Windows suportă şapte priorităţi relative pentru fire de execuţie : idle, lowest, below normal, normal, above normal, highest şi time-critical. Aceste priorităţi sunt relative la clasa de prioritate a procesului. Din nou, majoritatea firelor au prioritatea normal.

Prioritatea relativă a firelor de execuţie Descriere

Time-critical Firul rulează la 31 pentru clasa de prioritate real-time şi la 15 pentru toate celelalte clase de prioritate.

Highest Firul rulează cu două nivele peste normal.

Above normal Firul rulează cu un nivel peste normal. Normal Firul rulează normal pentru clasa de

prioritate a procesului. Below normal Firul rulează cu un nivel sub normal. Lowest Firul rulează cu două nivele sub normal. Idle Firul rulează cu prioritatea 16 pentru

clasa de prioritate real-time şi cu 1 pentru celelalte clase de prioritate.

În concluzie, procesul face parte dintr-o clasă de prioritate şi în cadrul acestuia atribuim priorităţi relative firelor de execuţie. Prioritatea absolută diferă de la un sistem de operare la altul. La Windows 2000 ea se calculează în modul următor:

Clasa de prioritate a procesului Prioritatea

reltivă a firului Idle Below

Normal Normal Above

Normal High Real-

Time Time-critical 15 15 15 15 15 31 Highest 6 8 10 12 15 26 Above normal 5 7 9 11 14 25 Normal 4 6 8 10 13 24 Below normal 3 5 7 9 12 23 Lowest 2 4 6 8 11 22 Idle 1 1 1 1 1 16

După cum se observă nu există nivelul de prioritate 0, care este rezervat.

De asemenea, nivelurile 17, 18, 19, 20, 21, 27, 28, 29 sau 30 pot fi obţinute doar dacă scriem un driver care rulează în mod nucleu. O aplicaţie utilizator nu poate obţine aceste priorităţi. De asemenea trebuie observat că un fir dintr-o clasa de prioritate

Page 22: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

real-time nu poate avea o prioritate mai mică de 16. De asemenea, un fir dintr-o clasă non-real-time nu poate avea o prioritate mai mare de 15.

Procesele nu sunt niciodată planificate, doar firele pot fi planificate. Clasa de prioritate a proceselor este o abstracţiune introdusă de Microsoft pentru a ne îndepărta de funcţionarea internă a planificatorului.

Un fir de execuţie cu prioritate ridicată ar trebui să execute mai puţine instrucţiuni, având acces la procesor aproape imediat, iar cele cu prioritate rămân planificabile pentru o perioada mai mare de timpi execută mai multe instrucţiuni. Dacă se respectă aceste reguli, întregul sistem de operare va răspunde mult mai repede la acţiunile utilizatorilor.

Programarea priorităţilor

Cum atribuim unui proces o clasă de prioritate în SDK? Atunci când apelăm CreateProcess, putem transmite clasa de prioritate dorită în parametrul fdwCreate.

O dată ce procesul copil rulează, el poate să îşi schimbe propria prioritate apelând SetPriorityClass :

Bool SetPriorityClass (HANDLE hProcess,

DWORD fdwPriority);

Această funcţie schimbă clasa de prioritate identificată de hProcess la valoarea specificată de parametrul fdwPriority. Acest parametru poate fi unul din identificatorii din tabelul de mai sus. Deoarece această funcţie ia identificatorul unui proces, putem schimba clasa de prioritate a oricărui proces care rulează in sistem atâta timp cât avem un identificator al lui şi avem accesul corespunzător.

În mod normal, un proces va încerca să îşi modifice propria sa clasă de prioritate.

Bool SetPriorityClass (GetCurrentProcess(), IDLE_PRIORITY_CLASS); O funcţie complementară folosită pentru a extrage clasa de prioritate a

unui proces este :

DWORD GetPriorityClass (HANDLE hProcess);

Funcţia returnează unul din identificatorii din tabelul de mai sus. Task Managerul din Windows 2000 permite utilizatorilor să schimbe

prioritatea unui proces. La crearea unui fir de execuţie, prioritatea sa relativă este întotdeauna

setată implicit la normal. Pentru a seta prioritatea unui fir, trebuie să apelăm funcţia SetThreadPriority :

BOOL SetThreadPriority (HANDLE Thread, int nPriority);

Bineînţeles, parametrul hThread identifică firul singular a cărui prioritate vrem să o schimbăm, iar parametrul nPriority este unul din cei 7 identificatori din tabelul de mai jos.

Prioritatea relativă a firului de execuţie

Constanta simbolică

Time-critical THREAD_PRIORIY_TIME_CRITICAL

Page 23: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

Highest THREAD_PRIORIY_TIME_HIGHEST Above normal THREAD_PRIORIY_TIME_ABOVE_NORMAL Normal THREAD_PRIORIY_TIME_NORMAL Below normal THREAD_PRIORIY_TIME_BELOW_NORMAL Lowest THREAD_PRIORIY_TIME_LOWEST Idle THREAD_PRIORIY_TIME_IDLE

Funcţia complementară care extrage prioritatea relativă a firului este:

int GetThreadPriority (HANDLE hThread);

Această funcţie returnează unul din identificatorii din tabelul de mai sus. În MFC putem trimite una din valorile de mai sus funcţiilor

AfxBeginThread şi CWinThread::SetThreadPriority. În SDK, CreateThread creează întotdeauna un nou fir cu prioritatea

relativă normal. Pentru a face ca firul să aibă prioritatea „idle”, putem transmite indicatorul CREATE_SUSPENDED funcţiei CreateThread. Acest lucru face ca firul să nu execute cod deloc. Putem apela apoi SetThreadPriority pentru a schimba prioritatea firului la prioritatea „idle”. Apoi apelăm ResumeThread astfel încât firul poate fi planificabil. Nu ştim când va primi mai mult timp de procesor, dar planificatorul ia in considerare faptul că acest fir are prioritatea „idle”. În cele din urmă, închidem identificatorul către noul fir astfel încât obiectul nucleu poate fi distrus de îndată ce firul îşi termină execuţia.

Windows nu oferă o funcţie care returnează nivelul de prioritate a unui fir de execuţie. Această omisiune este deliberată. Microsoft îşi rezervă dreptul de a schimba algoritmul de planificare în orice moment. Este recomandat să nu dezvoltăm o aplicaţie care are nevoie de cunoştinţe specifice ale alogoritmului planificatorului. Dacă rămânem cu clasele de prioritate ale proceselor şi cu nivelele relative de prioritate ale firelor de execuţie, aplicaţia noastră va rula bine atât în sistemul actual, cât şi în versiunile următoare.

Creşterea dinamică a nivelelor de prioritate

Sistemul determină nivelul de prioritate a firului de execuţie combinând nivelul relativ de prioritate al firului de execuţie cu clasa de prioritate a procesului firului. Acesta este uneori cunoscut sub numele de nivelul de prioritate de bază. Uneori, sistemul creşte nivelul de prioritate a unui fir – în mod normal ca răspuns la nişte evenimente I/O cum ar fi mesajele de fereastră sau citirea de pe disc.

De exemplu, un fir cu o prioritate normală într-un proces cu o clasă de prioritate ridicată are un nivel de prioritate de bază egal cu 13. Dacă utilizatorul apasă o tastă, sistemul plasează un mesaj WM_KEYDOWN în coada de mesaje a firului. Datorită apariţiei unui mesaj în coadă, firul devine planificabil. În plus, driverul tastaturii poate cere sistemului să crească temporar prioritatea firului. Astfel încât firul poate ajunge să aibă prioritatea 15.

Firul este planificat pentru o porţie de timp de procesor. De îndată ce această perioadă expiră, sistemul scade cu 1 prioritatea firului până la următoarea porţie de timp de procesor. Cea de-a treia execuţie a firului se va face efectua cu firul având prioritatea 13. Următoarele execuţii vor avea loc cu nivelul de prioritate 13, nivelul de prioritate de bază a firului.

Trebuie să notăm că nivelul de prioritate a unui fir nu scade niciodată mai jos de nivelul de prioritate de bază. De asemenea, driver-ul este cel care hotărăşte dimensiunea cu care trebuie mărit nivelul de prioritate. Din nou, Microsoft nu documentează mărimea creşterii nivelului de prioritate datorată unui driver. Acest

Page 24: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

lucru permite Microsoft-ului să regleze fin continuu creşterea dinamică pentru a determina cel mai bun timp de reacţie.

Sistemul creşte nivelul de prioritate doar pentru firele care un nivel de prioritate de bază între 1 şi 15. De fapt, de aceea acest interval este denumit intervalul de prioritate dinamică. În plus, sistemul nu creşte niciodată prioritatea unui fir în real-time. Deoarece firele din real-time efectuează majoritatea funcţiilor sistemului, setarea unei limite superioare pentru creşterea priorităţii previne situaţia în care o aplicaţie poate interfera cu sistemul de operare. De asemenea, sistemul nu creşte prioritatea în zona real-time (de la 16 la 31).

Unii dezvoltatori s-au plâns că creşterea dinamică a priorităţii a avut efecte negative asupra performanţei firelor, astfel încât Microsoft a adăugat două funcţii care ne permit să oprim creşterea dinamică a priorităţii : BOOL SetProcessPriorityBoost(HANDLE hProcess,

BOOL DisablePriorityBoost); BOOL SetThreadPriorityBoost( HANDLE hThread,

BOOL DisablePriorityBoost);

SetProcessPriorityBoost spune sistemului să activeze sau să dezactiveze creşterea dinamică a priorităţilor în interiorul unui proces; SetThreadPriorityBoost ne permite să facem acelaşi lucru pentru fire individuale de execuţie. Aceste funcţii au funcţii complementare care ne permit să determinăm dacă creşterea dinamică este activată sau nu : BOOL GetProcessPriorityBoost( HANDLE hProcess, PBOOL pDisablePriorityBoost); BOOL GetThreadPriorityBoost( HANDLE hThread,

PBOOL pDisablePriorityBoost);

Pentru fiecare din aceste funcţii, trebuie să transmitem identificatorul procesului sau al firului pentru care dorim să efectuăm aceste operaţii şi adresa unei variabile de tip BOOL care va fi setată de către funcţie.

Windows 98 nu oferă implementări utile pentru aceste patru funcţii. Ele returnează FALSE iar un apel ulterior al funcţiei GetLastError returnează ERROR_CALL_NOT_IMPLEMENTED.

Creşterea dinamică este aplicată şi în altă situaţie. Să ne imaginăm un fir de prioritate 4 care este privat de timp de procesor. Atunci când sistemul detectează ca acest fir a fost privat de timp de procesor între 3 şi 4 secunde, el va creşte dinamic prioritatea firului la 15 şi îi permite rularea pentru o perioadă dublă de timp. Atunci când aceasta expiră, nivelul de prioritate revine imediat la nivelul de prioritate de bază.

Apelarea funcţiilor membre MFC în afara graniţelor firelor de execuţie

În continuare vom prezenta partea proastă a scrierii de aplicaţii MFC cu fire de execuţie multiple. Atât timp cât firele nu apelează funcţii membru aparţinând obiectelor create de alte fire, sunt puţine restricţii cu privire la ceea ce pot face. Totuşi, dacă firul A transmite un pointer CWnd către pointerul B şi firul B apelează o funcţie membru a acelui obiect CWnd, este posibil ca MFC-ul să facă o aserţie într-o versiune cu depanare. O versiune release s-ar putea să funcţioneze corect, dar din nou, s-ar putea să nu funcţioneze corect. Există de asemenea posibilitatea să nu apară o aserţie într-o versiune cu depanare, dar nici aceasta nu va funcţiona corect. Totul depinde de ceea ce se întâmplă în interiorul cadrului de lucru când acel membru

Page 25: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

CWnd este apelat. Putem evita o mulţime de probleme compartimentând firele şi dând permisiunea fiecărui fir de a folosi doar acele obiecte pe care le creează mai degrabă decât să se bazeze pe obiecte create de alte fire. Dar în cazurile în care acest lucru nu este posibil, există câteva reguli pe care trebuie să le urmăm.

În primul rând, multe funcţii membru MFC pot fi apelate în siguranţă asupra unor obiecte din alte fire de execuţie. Majoritatea funcţiilor inline definite în fişierele INL din directorul Include al MFC-ului pot fi apelate şi în afara graniţelor firului deoarece ele sunt mai mult decât nişte containere pentru funcţiile API. Dar apelarea unei funcţii membru non-inline poate genera probleme. De exemplu, codul următor, care transmite un pointer namedpWnd de tip CWnd de la firul A către firul B şi în care B apelează CWnd::GetParent prin intermediul acestui pointer, funcţionează fără probleme:

CWinThread* pThread = AfxBeginThread(ThreadFunc, pWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = (CWnd*) pParam; CWnd* pParent = pWnd->GetParent();

return 0;

}

Prin simpla schimbare a lui GetParent cu GetParentFrame obţinem o aserţie:

CWinThread* pThread = AfxBeginThread(ThreadFunc, pWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = (CWnd*) pParam;

//Să ne pregătim pentru o aserţie. CWnd* pParent = pWnd->GetParentFrame(); return 0;

}

De ce funcţia GetParent funcţionează atunci când GetParentFrame nu funcţionează ? Deoarece apelul funcţiei GetParent conduce aproape direct către apelul funcţiei ::GetParent din API. Funcţia CWnd::GetParent este definită în modul următor în Afxwin2.ini: _AFXWIN_INLINE CWnd* CWnd::GetParent() const { ASSERT (::IsWindow(m_hWnd)) return CWnd::FromHandle (::GetParent (m_hWnd)); }

Acest cod nu prezintă nici o problemă; m_hWnd este valid deoarece este o parte a obiectului CWnd dat de pointerul pWnd şi funcţia FromHandle converteşte HWND-ul returnat de ::GetParent într-un pointer CWnd.

Să considerăm acum ce se întâmplă atunci când apelăm GetParentFrame, a cărei cod sursă se găseşte în fişierul Wincore.cpp. Linia care cauzează această eroare de inserare (assert error) este :

ASSERT_VALID (this);

ASSERT_VALID apelează CWnd::AssertValid, care efectuează o verificare logică asigurându-se că HWND-ul asociat cu this apare într-o hartă temporară sau permanentă de identificatori pe care cadrul de lucru o foloseşte pentru

Page 26: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

a converti HWND-urile în CWnd-uri. Calea de la un CWnd la HWND este simplă deoarece HWND este un membru de date a lui CWnd, dar calea de la un HWND la un CWnd este posibilă doar prin intermediu hărţilor de identificatori. Astfel apare o nouă problemă : hărţile de identificatori sunt locale fiecărui fir de execuţie şi nu sunt

vizibile pentru alte fire de execuţie. Dacă firul A creează un CWnd a cărui adresă este trecută către funcţia ASSERT_VALID, HWND-ul corespunzător nu va apărea în harta permanentă sau temporară de identificatori şi MFC-ul va cauza apariţia unei aserţii. Majoritatea funcţiilor membru ale MFC-ului apelează ASSERT_VALID, dar funcţiile inline nu fac acest lucru, cel puţin nu în distribuţiile curente.

În mod frecvent, aserţiile MFC-ului ne ajută să nu apelăm funcţii care oricum nu ar funcţiona. Într-o versiune release, funcţia GetParentFrame returnează valoarea NULL atunci când este apelată dintr-un fir diferit de cel din care cadrul părinte a fost creat. Dar în cazurile în care erorile de aserţie sunt false – acest lucru se întâmplă în cazurile în care funcţia funcţionează corect în ciuda tabelelor de identificatori pe fiecare fir – putem evita aserţiile transmiţând identificatori reali în locul pointerilor de obiecte. De exemplu, este mai sigur să apelăm CWnd::GetTopLevelParent într-un fir secundar dacă am apelat FromHandle în primul rând pentru a crea o intrare în harta permanentă sau temporară de identificatori, după cum este arătat în continuare:

CWinThread* pThread = AfxBeginThread (ThreadFunc, pWnd->m_hWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = CWnd::FromHandle ((HWND) pParam); CWnd* pParent = pWnd->GetTopLevelParent(); return 0; }

Din această cauză documentaţia MFC ne avertizează că ferestrele, obiectele GUI şi alte obiecte ar trebui transmise între fire folosind identificatori în locul pointerilor. În general, vom avea mai puţine probleme dacă transmitem identificatori şi folosim FromHandle pentru a recrea obiecte în firele destinaţie. Dar asta nu înseamnă că orice funcţie va merge.

Cum rămâne cu funcţiile membru aparţinând obiectelor create din clase „pure” MFC cum ar fi CDocument şi CRect, clase care nu împachetează HWND-uri, HDC-uri sau alte tipuri de identificatori şi din această cauză nu se bazează pe hărţi de identificatori ? Răspuns : unele merg şi altele nu merg. Nu este nici o problemă cu acest cod:

CWinThead* pThread = AfxBeginThread (ThreadFunc, pRect); UINT ThreadFunc (LPVOID pParam) { CRect* pRect = (CRect*) pParam; int nArea = pRect->Width() * pRect->Height(); return 0; }

Următorul cod însă va cauza apariţia unei aserţii :

CWinThread* pThread = AfxBeginThread (ThreadFunc, pDoc);

Page 27: Fire de execu ţieiasimin/pw/C6_2006.pdf · fir de execu ţie, solu ţia este de a atribui un timp maxim dup ă care sistemul s ă pauzeze execu ţia unui fir, permi ţând execu

UINT ThreadFunc (LPVOID pParam) { CDocument* pDoc = pParam;

pDoc->UpdateAllViews(NULL); return 0;

}

Chiar şi unele funcţii aparent inofensive cum ar fi AfxGetMainWnd nu funcţionează atunci când sunt apelate din altă parte decât din firul principal al aplicaţiei.

Important este ca înainte de a apela funcţii membru asupra obiectelor MFC create în alte fire de execuţie, trebuie să înţelegem implicaţiile. Şi unica modalitate de a face acest lucru este de a studia codul sursă MFC pentru a vedea cum se comportă o anumită funcţie membru. Trebuie de asemenea să ştim că MFC-ul nu este sigur din punct de vedere al firelor de execuţie. Aşa că chiar dacă o funcţie membru pare să fie sigură, trebuie să ne întrebăm ce s-ar putea întâmpla dacă firul B ar accesa un obiect creat de firul A si firul A si-ar începe execuţia , firul B fiind oprit brusc. Acest lucru este foarte greu de rezolvat şi doar creşte complexitatea scrierii de aplicaţii cu fire de execuţie multiple. Din această cauză în realitate aplicaţiile MFC cu fire de execuţie multiple tind să îşi execute majoritatea operaţiilor ţinând de interfaţa cu utilizatorul în firul principal de execuţie. Dacă un fir din fundal vrea să actualizeze interfaţa, el trimite sau adaugă un mesaj firului principal pentru ca firul principal să poată face actualizarea.