Firul executiei

Thread de execuție (thread; din engleză  thread  - thread) - cea mai mică unitate de procesare, a cărei execuție poate fi atribuită de nucleul sistemului de operare . Implementarea firelor de execuție și a proceselor diferă între sistemele de operare, dar în majoritatea cazurilor, firul de execuție rezidă în cadrul unui proces. Mai multe fire de execuție pot exista în cadrul aceluiași proces și pot partaja resurse precum memoria , în timp ce procesele nu partajează aceste resurse. În special, firele de execuție împărtășesc secvența de instrucțiuni a unui proces (codul său) și contextul acestuia - valorile variabilelor ( registrele procesorului și stiva de apeluri ) pe care le au la un moment dat.

Ca analogie, firele unui proces pot fi asemănate cu mai mulți bucătari care lucrează împreună. Toți gătesc același fel de mâncare, citesc aceeași carte de bucate cu aceeași rețetă și urmează instrucțiunile acesteia și nu neapărat citesc toți pe aceeași pagină.

Pe un singur procesor, multithreading -ul are loc de obicei prin multiplexare în timp (ca și în cazul multitasking -ului ): procesorul comută între diferite fire de execuție. Această schimbare de context are loc de obicei suficient de frecvent pentru ca utilizatorul să perceapă execuția firelor de execuție sau a sarcinilor ca fiind concurentă. În sistemele multiprocesor și multicore , firele de execuție sau sarcinile pot rula de fapt concomitent, fiecare procesor sau nucleu procesând un fir sau sarcină separat.

Multe sisteme de operare moderne acceptă atât tăierea timpului din planificatorul de procese, cât și firele de execuție multiprocesor. Nucleul sistemului de operare permite programatorilor să controleze firele de execuție printr-o interfață de apel de sistem . Unele implementări ale nucleului se referă la el ca un fir de nucleu , în timp ce altele se referă la el ca un proces ușor ( LWP ), care este un tip special de fir nucleu care împarte aceeași stare și date.  

Programele pot avea fire de execuție în spațiul utilizatorului atunci când firele de execuție sunt create folosind temporizatoare, semnale sau alte metode pentru a întrerupe execuția și a crea segmente de timp pentru o situație specifică ( Ad-hoc ).


Diferența față de procese

Firele de execuție diferă de procesele tradiționale ale sistemului de operare multitasking prin faptul că:

Se spune că sisteme precum Windows NT și OS/2 au fire „ieftine” și procese „costisitoare”. Pe alte sisteme de operare, diferența dintre firele de execuție și procese nu este la fel de mare, cu excepția costului comutării spațiului de adrese, care implică utilizarea unui buffer de traducere asociativ .

Multithreading

Multithreading, ca model de programare și execuție de cod larg răspândit, permite rularea mai multor fire într-un singur proces. Aceste fire de execuție împart resursele unui proces, dar pot rula și pe cont propriu. Modelul de programare cu mai multe fire oferă dezvoltatorilor o abstractizare convenabilă a execuției paralele. Cu toate acestea, poate cea mai interesantă aplicație a tehnologiei este atunci când este aplicată unui singur proces, ceea ce permite executarea acestuia în paralel pe un sistem multiprocesor .

Acest avantaj al unui program multithread îi permite să ruleze mai rapid pe sisteme de computer care au mai multe procesoare , un procesor cu mai multe nuclee sau pe un grup de mașini, deoarece firele de execuție de program se pretează în mod natural la execuția cu adevărat paralelă a proceselor. În acest caz, programatorul trebuie să fie foarte atent pentru a evita condițiile de cursă și alte comportamente neintuitive. Pentru a manipula corect datele, firele de execuție trebuie să treacă frecvent printr-o procedură de întâlnire pentru a procesa datele în ordinea corectă. Firele de execuție pot avea nevoie și de mutexuri (care sunt adesea implementate folosind semafore ) pentru a preveni modificarea simultană a datelor partajate sau citite în timpul procesului de modificare. Folosirea neglijentă a unor astfel de primitive poate duce la un impas .

O altă utilizare a multithreading-ului, chiar și pentru sistemele uniprocesor, este capacitatea unei aplicații de a răspunde la intrare. În programele cu un singur thread, dacă firul principal de execuție este blocat de executarea unei sarcini de lungă durată, întreaga aplicație poate fi în stare înghețată. Prin mutarea unor astfel de sarcini de lungă durată într-un fir de lucru care rulează în paralel cu firul principal, devine posibil ca aplicațiile să continue să răspundă la intrarea utilizatorului în timp ce sarcinile rulează în fundal. Pe de altă parte, în majoritatea cazurilor, multithreadingul nu este singura modalitate de a menține un program receptiv. Același lucru poate fi obținut prin I/O asincrone sau semnale în UNIX. [unu]

Sistemele de operare programează firele de execuție în unul dintre cele două moduri:

  1. Multithreadingul preventiv este în general considerat o abordare superioară, deoarece permite sistemului de operare să determine când ar trebui să aibă loc o schimbare de context. Dezavantajul multithreading-ului cu prioritate este că sistemul poate face o schimbare de context la momentul nepotrivit, ceea ce duce la inversarea priorității și alte efecte negative care pot fi evitate prin utilizarea multithreading-ului cooperativ.
  2. Multithreading cooperativ se bazează pe firele în sine și renunță la control dacă firele de execuție sunt la punctele de întrerupere. Acest lucru poate crea probleme dacă firul de execuție așteaptă o resursă până când aceasta devine disponibilă.

Până la sfârșitul anilor 1990, procesoarele desktop nu aveau suport pentru multithreading, deoarece comutarea între fire era în general mai lentă decât o comutare de context de proces complet . Procesoarele încorporate , care au cerințe mai mari pentru comportamentul în timp real , pot suporta multithreading prin reducerea timpului de comutare între fire, poate prin alocarea de fișiere de registru dedicate fiecărui fir de execuție, în loc să salveze/restaureze un fișier de registru comun. La sfârșitul anilor 1990, ideea de a executa instrucțiuni din mai multe fire în același timp, cunoscută sub numele de multithreading simultan, numit Hyper-Threading, a ajuns la computerele desktop cu procesorul Intel Pentium 4 . Apoi a fost exclus din procesoarele cu arhitectură Intel Core și Core 2 , dar ulterior restaurat în arhitectura Core i7 .

Criticii multithreading-ului susțin că creșterea utilizării thread-urilor are dezavantaje semnificative:

Deși firele de execuție par la un pas mic de la calculul secvenţial, ele reprezintă, de fapt, un salt uriaș. Ei renunță la cele mai importante și atractive proprietăți ale calculului secvenţial: comprehensibilitatea, predictibilitatea și determinismul. Firele de execuție, ca model de calcul, sunt remarcabil de nedeterministe, iar reducerea acestui non-determinism devine sarcina programatorului. [2]

Procese, fire de nucleu, fire de utilizare și fibre

Procesul este cea mai „grea” unitate de planificare a nucleului. Resursele proprii pentru proces sunt alocate de sistemul de operare. Resursele includ memorie, mânere de fișiere, socluri, mânere de dispozitiv și ferestre. Procesele utilizează spațiu de adresă și fișiere de resurse cu partajare în timp numai prin metode explicite, cum ar fi moștenirea descriptorilor de fișiere și a segmentelor de memorie partajată. Procesele sunt de obicei pre-convertite într-un mod de execuție multitasking.

Firele nucleului se numără printre unitățile „ușoare” ale programării nucleului. În cadrul fiecărui proces, există cel puțin un fir de execuție al nucleului. Dacă mai multe fire de execuție a nucleului pot exista într-un proces, acestea partajează aceeași memorie și fișier de resurse. Dacă procesul de execuție al planificatorului sistemului de operare este pus în prim-plan, atunci firele de execuție ale nucleului sunt, de asemenea, puse în prim plan multitasking. Firele de nucleu nu au resurse proprii, cu excepția stivei de apeluri , a unei copii a registrelor procesorului , inclusiv a contorului programului și a memoriei locale a firului de execuție (dacă există). Nucleul poate desemna un fir de execuție pentru fiecare nucleu logic al sistemului (deoarece fiecare procesor se împarte în mai multe nuclee logice dacă acceptă multithreading, sau acceptă doar un nucleu logic per nucleu fizic dacă nu acceptă multithreading), sau poate schimbă firele de execuție blocate. Cu toate acestea, firele de nucleu durează mult mai mult decât este nevoie pentru a schimba firele de execuție.

Firele de execuție sunt uneori implementate în spațiul utilizator al bibliotecilor, caz în care sunt numite fire de execuție utilizator . Nucleul nu știe despre ele, așa că sunt gestionate și programate în spațiul utilizatorului. În unele implementări , firele de execuție ale utilizatorului se bazează pe primele câteva fire de execuție ale nucleului pentru a profita de mașinile multiprocesor (modele M:N). În acest articol, termenul „thread” în mod implicit (fără calificativul „kernel” sau „personalizat”) se referă la „kernel thread”. Firele de execuție ale utilizatorului implementate cu ajutorul mașinilor virtuale sunt denumite și „firele verzi de execuție”. Firele personalizate de execuție sunt în general rapid de creat și ușor de gestionat, dar nu pot profita de multithreading și multiprocesare. Se pot bloca dacă toate firele de nucleu asociate cu acesta sunt ocupate, chiar dacă unele fire de execuție sunt gata de rulare.

Fibrele sunt unități de planificare și mai „ușoare” legate de multitasking cooperativ : o fibră care rulează trebuie să „cedeze” în mod explicit dreptul de a executa altor fibre, ceea ce face implementarea lor mult mai ușoară decât implementarea firelor de nucleu sau a firelor de utilizare. Fibrele pot fi programate să ruleze pe orice fir de execuție în cadrul aceluiași proces. Acest lucru permite aplicațiilor să obțină câștiguri de performanță prin gestionarea propriei planificări, mai degrabă decât să se bazeze pe planificatorul nucleului (care este posibil să nu fie configurat pentru a face acest lucru). Mediile de programare paralelă, cum ar fi OpenMP , își implementează de obicei sarcinile prin fibre.

Probleme cu fire și fibre

Concurență și structuri de date

Firele de execuție din același proces au același spațiu de adrese. Acest lucru permite executarea concomitentă a codurilor să fie strâns cuplate și să facă schimb de date în mod convenabil, fără costul general și complexitatea comunicării între procese . Atunci când mai multe fire de execuție partajează chiar și structuri de date simple, există pericolul unei condiții de concurență dacă este necesară mai mult de o instrucțiune de procesor pentru a actualiza datele: două fire de execuție pot ajunge să încerce să actualizeze structurile de date în același timp și să ajungă cu date a căror stare este diferită de cea așteptată. Erorile cauzate de condițiile de cursă pot fi foarte dificil de reprodus și izolat.

Pentru a evita acest lucru, firele de execuție ale interfețelor de programare a aplicațiilor (API) oferă primitive de sincronizare , cum ar fi mutexuri , pentru a bloca accesarea concomitentă a structurilor de date. Pe sistemele cu uniprocesor, un fir care accesează un mutex blocat trebuie să înceteze să ruleze și, prin urmare, să inițieze o comutare de context. Pe sistemele multiprocesor, un fir de execuție poate obține un spinlock în loc să interogheze mutexul . Ambele metode pot degrada performanța și pot forța procesorul din sistemele SMP să concureze pentru magistrala de memorie, mai ales dacă nivelul de modularitate de blocare este prea mare.

I/O și programare

Implementarea firelor și fibrelor personalizate se face de obicei în întregime în spațiul utilizatorului. Ca rezultat, schimbarea contextului între firele utilizator și fibre în același proces este foarte eficientă, deoarece nu necesită deloc interacțiune cu nucleul. O comutare de context este efectuată local prin salvarea registrelor procesorului utilizați de un fir de execuție sau o fibră de utilizator și apoi încărcând registrele necesare pentru o nouă execuție. Deoarece programarea are loc în spațiul utilizatorului, politica de programare poate fi adaptată cu ușurință la cerințele unui anumit program.

Cu toate acestea, utilizarea blocărilor de apel de sistem pentru firele de execuție ale utilizatorului (spre deosebire de firele de execuție a nucleului) și fibre are propriile sale probleme. Dacă un fir de execuție sau o fibră de utilizator execută un apel de sistem, alte fire și fibre din proces nu pot rula până când acea procesare este completă. Un exemplu tipic al unei astfel de probleme este legat de performanța operațiunilor I/O. Majoritatea programelor sunt proiectate pentru a efectua I/O sincron. Când se inițiază un I/O, se efectuează un apel de sistem și acesta nu revine până când este finalizat. Între timp, întregul proces este blocat de nucleu și nu poate rula, făcând inoperabile alte fire de execuție ale utilizatorului și fibre ale procesului.

O soluție obișnuită la această problemă este furnizarea unui API I/O separat care implementează o interfață sincronă folosind I/O interne neblocante și pornirea unui alt fir sau fibră de utilizator în timp ce I/O este procesată. Soluții similare pot fi furnizate pentru blocarea apelurilor de sistem. În plus, programul poate fi scris pentru a evita utilizarea I/O sincronă sau alte apeluri de sistem de blocare.

SunOS 4.x a introdus așa-numitele „ procese ușoare ” sau LWP-uri . NetBSD 2.x + și DragonFly BSD au implementat LWP ca fire de nucleu (model 1:1). SunOS 5.2 și până la SunOS 5.8 și NetBSD 2 și până la NetBSD 4, au implementat un model pe două niveluri folosind unul sau mai multe fire de execuție pentru fiecare fir de nucleu (model M:N). SunOS 5.9 și versiunile ulterioare și NetBSD 5 au eliminat suportul pentru firele de execuție, revenind la un model 1:1. [3] FreeBSD 5 a implementat modelul M:N. FreeBSD 6 acceptă atât modelele 1:1, cât și M:N, iar utilizatorul poate alege pe care să îl folosească într-un anumit program folosind /etc/libmap.conf. În FreeBSD versiunea 7, modelul 1:1 a devenit implicit, iar în FreeBSD 8 și mai târziu, modelul M:N nu este acceptat deloc.

Utilizarea firelor de execuție simplifică codul utilizatorului prin mutarea unora dintre aspectele mai complexe ale multithreading-ului în nucleu. Programul nu este necesar să programeze fire de execuție și capturi explicite ale procesorului. Codul utilizatorului poate fi scris în stilul procedural familiar, inclusiv blocarea apelurilor API, fără a interzice accesul altor fire la procesor. Cu toate acestea, firele de execuție ale nucleului pot provoca o schimbare de context între firele de execuție în orice moment și, prin urmare, pot expune erorile de concurență și cursă care ar putea să nu apară. Pe sistemele SMP, acest lucru este și mai exacerbat, deoarece firele de nucleu pot rula literalmente simultan pe procesoare diferite.

Modele

1:1 (file la nivel de kernel)

Firele de execuție create de utilizator în modelul 1-1 corespund entităților de nucleu dispecerabile. Aceasta este cea mai simplă implementare posibilă a threading-ului. API -ul Windows a adoptat această abordare încă de la început. Pe Linux, biblioteca obișnuită C implementează această abordare (prin POSIX Thread Library , iar în versiunile mai vechi, prin LinuxThreads ). Aceeași abordare este utilizată de sistemul de operare Solaris , NetBSD și FreeBSD .

N:1 (file la nivel de utilizator)

Modelul N:1 presupune că toate firele de execuție la nivel de utilizator se mapează la o singură entitate de programare la nivel de kernel, iar nucleul nu știe nimic despre compoziția firelor de execuție a aplicației. Cu această abordare, schimbarea contextului se poate face foarte rapid și, în plus, poate fi implementată chiar și pe nuclee simple care nu acceptă multithreading. Cu toate acestea, unul dintre principalele sale dezavantaje este că nu poate profita de accelerarea hardware pe procesoarele cu mai multe fire sau pe computerele cu mai multe procese, deoarece doar un singur fir de execuție poate fi programat la un moment dat. Acest model este folosit în GNU Portable Threads.

M:N (streaming mixt)

În modelul M:N, un număr M de fire de execuție a aplicației sunt mapate la un număr N de entități de nucleu sau „procesoare virtuale”. Modelul este un compromis între modelul la nivel de kernel ("1:1") și modelul la nivel de utilizator ("N:1"). În general vorbind, firul de execuție al sistemului „M:N” este mai complex de implementat decât firele de execuție ale nucleului sau utilizatorului, deoarece nu sunt necesare modificări de cod nici pentru nucleu, nici pentru spațiul utilizatorului. Într-o implementare M:N, biblioteca de fire este responsabilă pentru programarea firelor de execuție ale utilizatorului pe entitățile de programare disponibile. În același timp, schimbarea contextului firelor se face foarte rapid, deoarece modelul evită apelurile de sistem. Cu toate acestea, complexitatea și probabilitatea inversărilor prioritare cresc, precum și programarea neoptimală fără o coordonare extinsă (și costisitoare) între planificatorul utilizatorului și planificatorul nucleului.

Implementări

Există multe implementări diferite, incompatibile de fire. Acestea includ atât implementări la nivel de kernel, cât și implementări la nivel de utilizator. Cel mai adesea ele aderă mai mult sau mai puțin strâns la standardul de interfață POSIX Threads .

Exemple de implementări de fire la nivel de kernel

  • Fire nuclee ușoare (LWKT) în diferite versiuni BSD
  • Filetarea MxN
  • POSIX Threads Library (NPTL) pentru Linux , o implementare a standardului POSIX Threads
  • Apple Multiprocessing Services, versiunea 2.0 și ulterioară, utilizează un microkernel încorporat în Mac OS 8.6, modificat în versiunile ulterioare în scopuri de întreținere.
  • Windows începând cu Windows 95 , Windows NT și după.

Exemple de implementări de fire la nivel de utilizator

  • GNU Portable Threads
  • FSU Pthreads
  • Apple Thread Manager
  • REALbasic (inclusiv un API pentru partajarea firelor)
  • Netscape Portable Runtime (inclusiv implementarea fibrelor în spațiul utilizatorului)

Exemple de implementări de flux mixt

  • „Activările planificatorului” sunt utilizate în biblioteca de aplicații de threading POSIX nativă a NetBSD (modelul M:N spre deosebire de modelul de aplicație nucleu 1:1 sau spațiu utilizator)
  • Marcel de la proiectul PM2
  • OS pentru supercomputer Tera/Cray MTA
  • Windows 7

Exemple de implementări de fibră

Fibrele pot fi implementate fără suport pentru sistemul de operare, deși unele sisteme de operare și biblioteci oferă suport explicit pentru ele.

  • Biblioteca Win32 conține API pentru fibre [4] (Windows NT 3.51 SP3 și mai târziu)
  • Ruby ca implementare a „ firelor verzi

Suport limbaj de programare

Multe limbaje de programare acceptă firele în mod diferit. Majoritatea implementărilor C și C++ (înainte de standardul C++11) nu oferă suport direct pentru firele în sine, dar oferă acces la firele furnizate de sistemul de operare printr- un API . Unele limbaje de programare de nivel superior (de obicei multiplatformă), cum ar fi Java , Python și .NET , oferă dezvoltatorului threading ca un abstract, specific platformei, distinct de implementarea de execuție de către dezvoltator a firelor de execuție. O serie de alte limbaje de programare încearcă, de asemenea, să abstragă complet conceptul de concurență și threading de la dezvoltator ( Cilk , OpenMP , MPI ...). Unele limbi sunt concepute special pentru concurență (Ateji PX, CUDA ).

Unele limbaje de programare interpretativă, cum ar fi Ruby și CPython (o implementare a lui Python), acceptă fire de execuție, dar au o limitare cunoscută sub numele de Global Interpreter Lock (GIL). GIL este un mutex de excepție executat de interpret, care poate împiedica interpretul să interpreteze simultan codul aplicației în două sau mai multe fire de execuție în același timp, limitând efectiv concurența pe sistemele cu mai multe nuclee (în principal pentru fire legate de procesor, nu fire legate de rețea). ). ).

Vezi și

Note

  1. Serghei Ignatchenko. Single-threading: Înapoi în viitor? Arhivat pe 29 septembrie 2011 la Wayback Machine Overload #97
  2. Edward A. Lee. Problema cu firele . Raport tehnic nr. UCB/EECS-2006-1 . Departamentul EECS, Universitatea din California, Berkeley (10 ianuarie 2006). Preluat la 30 mai 2012. Arhivat din original la 26 iunie 2012.
  3. Oracle și Soare (downlink) . Consultat la 30 noiembrie 2011. Arhivat din original pe 27 martie 2009. 
  4. CreateFiber Arhivat pe 17 iulie 2011 la Wayback Machine MSDN

Literatură

  • David R. Butenhof. Programare cu fire POSIX. Addison Wesley. ISBN 0-201-63392-2
  • Bradford Nichols, Dick Buttlar, Jacqueline Proulx Farell. Programare Pthread. O'Reilly și Asociații. ISBN 1-56592-115-1
  • Charles J. Northrup. Programare cu fire UNIX. John Wiley & Sons. ISBN 0-471-13751-0
  • Mark Walmsley. Programare cu mai multe fire în C++. Springer. ISBN 1-85233-146-1
  • Paul Hyde. Programare Java Thread. Sams. ISBN 0-672-31585-8
  • Bill Lewis. Threads Primer: A Guide to Multithreaded Programming. Prentice Hall. ISBN 0-13-443698-9
  • Steve Kleiman, Devang Shah, Bart Smaalders. Programare cu fire, SunSoft Press. ISBN 0-13-172389-8
  • Pat Villani. Programare WIN32 avansată: Fișiere, fire și sincronizare proces. Editura Harpercollins. ISBN 0-87930-563-0
  • Jim Beveridge, Robert Wiener. Aplicații multithreading în Win32. Addison Wesley. ISBN 0-201-44234-5
  • Thuan Q. Pham, Pankaj K. Garg. Programare multifile cu Windows NT. Prentice Hall. ISBN 0-13-120643-5
  • Len Dorfman, Marc J. Neuberger. Multithreading eficient în OS/2. McGraw-Hill Osborne Media. ISBN 0-07-017841-0
  • Alan Burns, Andy Wellings. Concurență în ADA. Cambridge University Press. ISBN 0-521-62911-X
  • Uresh Vahalia. Interne Unix: noile frontiere. Prentice Hall. ISBN 0-13-101908-2
  • Alan L. Dennis. .Net multithreading. Compania de publicații Manning. ISBN 1-930110-54-5
  • Tobin Titus, Fabio Claudio Ferracchiati, Srinivasa Sivakumar, Tejaswi Redkar, Sandra Gopikrishna. C# Threading Manual. Informații de la egal la egal. ISBN 1-86100-829-5
  • Tobin Titus, Fabio Claudio Ferracchiati, Srinivasa Sivakumar, Tejaswi Redkar, Sandra Gopikrishna. Manual Visual Basic .Net Threading. Wrox Press. ISBN 1-86100-713-2

Link -uri