În ce scop sunt utilizate sistemele cu mai multe fire. Opt reguli simple pentru dezvoltarea aplicațiilor cu mai multe fire

Ce subiect ridică cele mai multe întrebări și dificultăți pentru începători? Când l-am întrebat pe profesorul meu și programatorul Java Alexander Pryakhin despre acest lucru, el a răspuns imediat: „Multithreading”. Mulțumesc lui pentru idee și ajutor la pregătirea acestui articol!

Ne vom uita în lumea interioară a aplicației și a proceselor sale, vom afla care este esența multithreading-ului, când este utilă și cum să o implementăm - folosind Java ca exemplu. Dacă învățați o altă limbă OOP, nu vă faceți griji: principiile de bază sunt aceleași.

Despre fluxuri și originile lor

Pentru a înțelege multithreading, să înțelegem mai întâi ce este un proces. Un proces este o bucată de memorie virtuală și resurse pe care sistemul de operare le alocă pentru a rula un program. Dacă deschideți mai multe instanțe ale aceleiași aplicații, sistemul va aloca un proces pentru fiecare. În browserele moderne, un proces separat poate fi responsabil pentru fiecare filă.

Probabil că ați dat peste „Managerul de activități” Windows (în Linux este „Monitorul sistemului”) și știți că procesele care rulează inutile încarcă sistemul, iar cele mai „grele” dintre ele înghețează adesea, deci trebuie să fie terminate forțat .

Dar utilizatorilor le place multitasking-ul: nu le hrăniți cu pâine - lăsați-i să deschidă o duzină de ferestre și să sară înainte și înapoi. Există o dilemă: trebuie să asigurați funcționarea simultană a aplicațiilor și, în același timp, să reduceți sarcina sistemului, astfel încât să nu încetinească. Să presupunem că hardware-ul nu poate ține pasul cu nevoile proprietarilor - trebuie să rezolvați problema la nivel de software.

Vrem ca procesorul să execute mai multe instrucțiuni și să proceseze mai multe date pe unitate de timp. Adică, trebuie să încadrăm mai multe coduri executate în fiecare felie de timp. Gândiți-vă la o unitate de execuție a codului ca la un obiect - acesta este un fir.

Un caz complex este mai ușor de abordat dacă îl împărțiți în mai multe cazuri simple. La fel se întâmplă atunci când se lucrează cu memorie: un proces „greu” este împărțit în fire, care ocupă mai puține resurse și sunt mai predispuse să livreze codul la calculator (cum mai exact - vezi mai jos).

Fiecare aplicație are cel puțin un proces și fiecare proces are cel puțin un fir, care se numește fir principal și din care sunt lansate altele noi, dacă este necesar.

Diferența dintre fire și procese

    Firele utilizează memoria alocată procesului, iar procesele necesită propriul spațiu de memorie. Prin urmare, thread-urile sunt create și finalizate mai repede: sistemul nu trebuie să le aloce de fiecare dată un spațiu de adrese nou și apoi eliberați-l.

    Procesele fiecare funcționează cu propriile date - pot schimba ceva numai prin mecanismul de comunicare între procese. Firele accesează reciproc datele și resursele direct: ceea ce s-a schimbat este disponibil imediat pentru toată lumea. Firul poate controla „colegul” din proces, în timp ce procesul își controlează exclusiv „fiicele”. Prin urmare, comutarea între fluxuri este mai rapidă și comunicarea între ele este mai ușoară.

Care este concluzia din aceasta? Dacă trebuie să procesați o cantitate mare de date cât mai repede posibil, împărțiți-le în bucăți care pot fi procesate prin fire separate, apoi împărțiți rezultatul. Este mai bine decât a da naștere proceselor înfometate de resurse.

Dar de ce o aplicație populară precum Firefox merge pe calea creării mai multor procese? Deoarece pentru browser funcționează filele izolate este fiabil și flexibil. Dacă ceva nu este în regulă cu un proces, nu este necesar să încheiați întregul program - este posibil să salvați cel puțin o parte din date.

Ce este multithreading

Așa că ajungem la punctul principal. Multithreading este atunci când procesul de aplicare este împărțit în fire care sunt procesate în paralel - la o unitate de timp - de către procesor.

Sarcina de calcul este distribuită între două sau mai multe nuclee, astfel încât interfața și alte componente ale programului să nu încetinească munca celuilalt.

Aplicațiile multi-thread pot fi rulate și pe procesoare single-core, dar apoi thread-urile sunt executate pe rând: primul a funcționat, starea acestuia a fost salvată - al doilea a fost lăsat să funcționeze, salvat - a revenit la primul sau a lansat al treilea, etc.

Oamenii ocupați se plâng că au doar două mâini. Procesele și programele pot avea cât mai multe mâini necesare pentru a finaliza sarcina cât mai repede posibil.

Așteptați un semnal: sincronizare în aplicații multi-thread

Imaginați-vă că mai multe fire încearcă să schimbe aceeași zonă de date în același timp. A cui schimbări vor fi acceptate în cele din urmă și ale căror modificări vor fi anulate? Pentru a evita confuzia atunci când se ocupă de resurse partajate, firele trebuie să își coordoneze acțiunile. Pentru a face acest lucru, fac schimb de informații folosind semnale. Fiecare fir le spune celorlalți ce face și ce schimbări de așteptat. Deci, datele tuturor firelor despre starea curentă a resurselor sunt sincronizate.

Instrumente de sincronizare de bază

Excludere mutuala (excludere reciprocă, abreviată - mutex) - un „flag” care merge la firul care este permis în prezent să funcționeze cu resurse partajate. Elimină accesul altor fire la zona de memorie ocupată. Într-o aplicație pot exista mai multe mutexe și pot fi partajate între procese. Există o problemă: mutex forțează aplicația să acceseze de fiecare dată nucleul sistemului de operare, ceea ce este costisitor.

Semafor - vă permite să limitați numărul de fire care pot accesa o resursă la un moment dat. Acest lucru va reduce încărcarea procesorului atunci când se execută codul acolo unde există blocaje. Problema este că numărul optim de fire depinde de computerul utilizatorului.

Eveniment - definiți o condiție la apariția căreia controlul este transferat la firul dorit. Fluxurile fac schimb de date despre evenimente pentru a dezvolta și a continua acțiunile reciproc. Una a primit datele, cealaltă a verificat corectitudinea acesteia, a treia a salvat-o pe hard disk. Evenimentele diferă prin modul în care sunt anulate. Dacă trebuie să anunțați mai multe fire despre un eveniment, va trebui să setați manual funcția de anulare pentru a opri semnalul. Dacă există un singur fir țintă, puteți crea un eveniment de resetare automată. Va opri semnalul însuși după ce va ajunge la flux. Evenimentele pot fi puse în coadă pentru un control flexibil al fluxului.

Secțiunea critică - un mecanism mai complex care combină un contor de buclă și un semafor. Contorul vă permite să amânați începutul semaforului pentru timpul dorit. Avantajul este că nucleul este activat numai dacă secțiunea este ocupată și semaforul trebuie activat. În restul timpului, firul rulează în modul utilizator. Din păcate, o secțiune poate fi utilizată numai într-un singur proces.

Cum se implementează multithreading în Java

Clasa Thread este responsabilă pentru lucrul cu fire în Java. Crearea unui thread nou pentru a executa o sarcină înseamnă crearea unei instanțe din clasa Thread și asocierea acesteia cu codul dorit. Acest lucru se poate face în două moduri:

    subclasa Fir;

    implementați interfața Runnable în clasa dvs. și apoi treceți instanțele clasei la constructorul Thread.

Deși nu vom aborda subiectul blocajelor, atunci când firele se blochează reciproc munca și se blochează, vom lăsa asta pentru articolul următor.

Exemplu multithreading Java: ping-pong cu mutute

Dacă credeți că ceva teribil este pe cale să se întâmple, expirați. Vom lua în considerare lucrul cu obiectele de sincronizare aproape într-un mod jucăuș: două fire vor fi aruncate de un mutex. Dar, de fapt, veți vedea o aplicație reală în care un singur fir poate procesa date disponibile publicului la un moment dat.

Mai întâi, să creăm o clasă care moștenește proprietățile firului pe care îl știm deja și să scriem o metodă kickBall:

Clasa publică PingPongThread extinde firul (PingPongThread (numele șirului) (this.setName (nume); // suprascrie numele firului) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (ball);)) private void kickBall (Ball ball) (if (! ball.getSide (). egal cu (getName ())) (ball.kick (getName ());)))

Acum să ne ocupăm de minge. El nu va fi simplu cu noi, ci memorabil: astfel încât să poată spune cine l-a lovit, din ce parte și de câte ori. Pentru a face acest lucru, folosim un mutex: va colecta informații despre activitatea fiecărui fir - acest lucru va permite firelor izolate să comunice între ele. După a 15-a lovitură, vom scoate mingea din joc, pentru a nu o răni grav.

Clasa publică Ball (private int kicks = 0; private static Ball instance = new Ball (); private String side = ""; private Ball () () static Ball getBall () (instanță de retur;) lovitură nulă sincronizată (String nume de joc)) (Kicks ++; side = playername; System.out.println (Kicks + "" + side);) String getSide () (return side;) boolean isInGame () (return (Kicks)< 15); } }

Și acum două fire de jucători intră în scenă. Să le numim, fără alte întrebări, Ping și Pong:

Clasa publică PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () throws1 InterruptedException player) .start (); player2.start ();))

"Stadion plin de oameni - este timpul să începem meciul." Vom anunța oficial deschiderea ședinței - în clasa principală a aplicației:

Clasa publică PingPong (public static void main (String args) aruncă InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

După cum puteți vedea, nu este nimic furios aici. Aceasta este doar o introducere în multithreading pentru moment, dar știți deja cum funcționează și puteți experimenta - limitați durata jocului nu cu numărul de lovituri, ci cu timpul, de exemplu. Vom reveni la tema multithreading mai târziu - ne vom uita la pachetul java.util.concurrent, la biblioteca Akka și la mecanismul volatil. Să vorbim și despre implementarea multithreading-ului în Python.

Programarea multithread nu este fundamental diferită de scrierea de interfețe grafice de utilizator bazate pe evenimente sau chiar scrierea de aplicații secvențiale simple. Toate regulile importante care reglementează încapsularea, separarea preocupărilor, cuplarea liberă etc. se aplică aici. Dar multor dezvoltatori le este greu să scrie programe multithread tocmai pentru că neglijează aceste reguli. În schimb, încearcă să pună în practică cunoștințele mult mai puțin importante despre fire și primitive de sincronizare, culese din textele despre programarea multithreading pentru începători.

Deci, care sunt aceste reguli

Un alt programator, care se confruntă cu o problemă, crede: „Oh, exact, trebuie să aplicăm expresii regulate”. Și acum are deja două probleme - Jamie Zawinski.

Un alt programator, care se confruntă cu o problemă, crede: „A, bine, voi folosi fluxurile aici”. Și acum are zece probleme - Bill Schindler.

Prea mulți programatori care se angajează să scrie cod multi-threaded cad în capcană, ca eroul baladei lui Goethe " ucenicul vrajitor". Programatorul va învăța cum să creeze o grămadă de fire care, în principiu, funcționează, dar mai devreme sau mai târziu scapă de sub control, iar programatorul nu știe ce să facă.

Dar, spre deosebire de abandonul vrăjitorului, programatorul nefericit nu poate spera la sosirea unui vrăjitor puternic care își va flutura bagheta și va restabili ordinea. În schimb, programatorul merge pentru cele mai inestetice trucuri, încercând să facă față problemelor care apar în mod constant. Rezultatul este întotdeauna același: se obține o aplicație prea complicată, limitată, fragilă și nesigură. Are amenințarea perpetuă a blocajului și a altor pericole inerente codului multithread rău. Nici măcar nu vorbesc despre blocări inexplicabile, performanțe slabe, rezultate de muncă incomplete sau incorecte.

Poate v-ați întrebat: de ce se întâmplă acest lucru? O concepție greșită obișnuită este: „Programarea multi-thread este foarte dificilă”. Dar nu este cazul. Dacă un program multi-thread nu este de încredere, atunci eșuează de obicei din aceleași motive ca un program single-thread de calitate scăzută. Doar că programatorul nu urmează metodele de dezvoltare fundamentale, bine cunoscute și dovedite. Programele multithread par doar a fi mai complexe, deoarece cu cât firele paralele merg mai prost, cu atât fac mai multe mizerie - și mult mai rapid decât ar face un singur thread.

Concepția greșită despre „complexitatea programării multi-threaded” a devenit răspândită din cauza acelor dezvoltatori care s-au dezvoltat profesional în scrierea codului single-threaded, întâi au întâmpinat multithreading și nu au făcut față acestuia. Dar, în loc să-și regândească prejudecățile și obiceiurile de muncă, ei se încăpățânează să rezolve faptul că nu vor să lucreze în niciun fel. Scuzând software-uri nesigure și termenele ratate, acești oameni repetă același lucru: „programarea multithreaded este foarte dificilă”.

Vă rugăm să rețineți că mai sus vorbesc despre programe tipice care utilizează multithreading. Într-adevăr, există scenarii complexe cu mai multe fire - precum și scenarii complexe cu un singur fir. Dar ele nu sunt comune. De regulă, în program nu se cere nimic supranatural de la programator. Mutăm datele, le transformăm, facem calcule din când în când și, în cele din urmă, salvăm informațiile într-o bază de date sau le afișăm pe ecran.

Nu este nimic dificil în îmbunătățirea programului mediu cu un singur fir și transformarea acestuia într-unul cu mai multe fire. Cel puțin nu ar trebui să fie. Dificultăți apar din două motive:

  • programatorii nu știu cum să aplice metode de dezvoltare dovedite simple, bine cunoscute;
  • majoritatea informațiilor prezentate în cărți despre programarea multi-thread sunt corecte din punct de vedere tehnic, dar complet neaplicabile pentru rezolvarea problemelor aplicate.

Cele mai importante concepte de programare sunt universale. Acestea se aplică în mod egal programelor cu un singur fir și cu mai multe fire. Programatorii care s-au înecat într-o vâlvătaie de fluxuri pur și simplu nu au învățat lecții importante atunci când au stăpânit codul cu un singur fir. Pot spune asta pentru că astfel de dezvoltatori fac aceleași greșeli fundamentale în programele multi-thread și single-thread.

Poate cea mai importantă lecție care trebuie învățată în șaizeci de ani de istorie a programării este: stare mutabilă globală- rău... Rău adevărat. Programele care se bazează pe o stare mutabilă la nivel global sunt relativ greu de argumentat și, în general, sunt de încredere, deoarece există prea multe modalități de a schimba starea. Au existat o mulțime de studii care confirmă acest principiu general, există nenumărate modele de proiectare, al căror obiectiv principal este implementarea într-un fel sau altul de ascundere a datelor. Pentru a vă face programele mai previzibile, încercați să eliminați starea mutabilă cât mai mult posibil.

Într-un program serial cu un singur fir, probabilitatea corupției datelor este direct proporțională cu numărul de componente care pot modifica datele.

De regulă, nu este posibil să scăpați complet de statul global, dar dezvoltatorul are instrumente foarte eficiente în arsenalul său, care vă permit să controlați cu strictețe ce componente ale programului pot schimba starea. În plus, am învățat cum să creăm straturi API restrictive în jurul structurilor de date primitive. Prin urmare, avem un control bun asupra modului în care se schimbă aceste structuri de date.

Problemele statului mutabil la nivel global au devenit treptat evidente la sfârșitul anilor '80 și începutul anilor '90, odată cu proliferarea programării bazate pe evenimente. Programele nu au mai început „de la început” sau au urmat o singură cale previzibilă de execuție „până la sfârșit”. Programele moderne au o stare inițială, după ce au ieșit din care apar evenimente în ele - într-o ordine imprevizibilă, cu intervale de timp variabile. Codul rămâne cu un singur fir, dar devine deja asincron. Probabilitatea corupției datelor crește tocmai pentru că ordinea de apariție a evenimentelor este foarte importantă. Situațiile de acest fel sunt destul de frecvente: dacă evenimentul B apare după evenimentul A, atunci totul funcționează bine. Dar dacă evenimentul A apare după evenimentul B, iar evenimentul C are timp să intervină între ele, atunci datele pot fi distorsionate dincolo de recunoaștere.

Dacă sunt implicate fluxuri paralele, problema este în continuare agravată, deoarece mai multe metode pot opera simultan pe starea globală. Devine imposibil să judecăm exact cum se schimbă statul global. Vorbim deja nu numai despre faptul că evenimentele pot avea loc într-o ordine imprevizibilă, ci și despre faptul că starea mai multor fire de execuție poate fi actualizată. simultan... Cu programarea asincronă, vă puteți asigura, cel puțin, că un anumit eveniment nu se poate întâmpla înainte ca un alt eveniment să termine procesarea. Adică, este posibil să spunem cu certitudine care va fi statul global la sfârșitul procesării unui anumit eveniment. În codul cu mai multe fire, de regulă, este imposibil să se spună care evenimente vor avea loc în paralel, deci este imposibil să se descrie cu certitudine starea globală în orice moment din timp.

Un program multithreaded cu o amplă stare mutabilă la nivel global este unul dintre cele mai elocvente exemple ale principiului incertitudinii lui Heisenberg pe care îl cunosc. Este imposibil să verificați starea unui program fără a-i modifica comportamentul.

Când încep o altă filippică despre starea globală mutabilă (esența este prezentată în paragrafele anterioare), programatorii își dau ochii peste cap și mă asigură că știu toate acestea de mult timp. Dar dacă știi asta, de ce nu poți să îți dai seama din codul tău? Programele sunt pline de stări mutabile globale, iar programatorii se întreabă de ce codul nu funcționează.

În mod surprinzător, cea mai importantă lucrare în programarea multithread se întâmplă în timpul fazei de proiectare. Este necesar să se definească în mod clar ce ar trebui să facă programul, să se dezvolte module independente pentru a îndeplini toate funcțiile, să se descrie în detaliu ce date sunt necesare pentru fiecare modul și să se determine modalitățile de schimb de informații între module ( Da, nu uitați să pregătiți tricouri frumoase pentru toți cei implicați în proiect. Primul lucru.- aproximativ ed. în original). Acest proces nu este fundamental diferit de proiectarea unui program cu un singur fir. Cheia succesului, ca și în cazul codului cu un singur fir, este limitarea interacțiunilor dintre module. Dacă puteți scăpa de starea mutabilă partajată, problemele de partajare a datelor pur și simplu nu vor apărea.

Cineva ar putea susține că uneori nu există timp pentru o proiectare atât de delicată a programului, ceea ce va face posibilă lipsa statului global. Cred că este posibil și necesar să petreci timp în acest sens. Nimic nu afectează programele multithreaded la fel de distructiv ca încercarea de a face față stării mutabile globale. Cu cât trebuie să gestionați mai multe detalii, cu atât este mai probabil ca programul dvs. să atingă vârful și să cadă.

În aplicațiile realiste, trebuie să existe o anumită stare partajată care se poate schimba. Și aici este locul în care majoritatea programatorilor încep să aibă probleme. Programatorul vede că este necesară o stare partajată aici, se îndreaptă spre arsenalul cu mai multe fire și ia de acolo cel mai simplu instrument: o blocare universală (secțiune critică, mutex, sau cum o numesc ei). Se pare că excluderea reciprocă va rezolva toate problemele de partajare a datelor.

Numărul de probleme care pot apărea cu o astfel de blocare este uimitor. Trebuie luate în considerare condițiile cursei, problemele de blocare cu blocare excesivă și problemele de echitate alocării sunt doar câteva exemple. Dacă aveți mai multe blocări, mai ales dacă acestea sunt imbricate, atunci va trebui, de asemenea, să luați măsuri împotriva blocării, blocării dinamice, a cozilor de blocare și a altor amenințări asociate concurenței. În plus, există probleme inerente de blocare.
Când scriu sau revizuiesc codul, am o regulă de fier aproape infailibilă: dacă ai făcut o încuietoare, atunci parcă ai făcut o greșeală undeva.

Această afirmație poate fi comentată în două moduri:

  1. Dacă aveți nevoie de blocare, atunci probabil că aveți o stare globală mutabilă pe care doriți să o protejați împotriva actualizărilor simultane. Prezența stării mutabile globale este un defect în faza de proiectare a aplicației. Examinați și reproiectați.
  2. Utilizarea corectă a blocărilor nu este ușoară și poate fi incredibil de dificil să localizați erorile legate de blocare. Este foarte probabil să utilizați incuietoarea în mod incorect. Dacă văd o blocare și programul se comportă într-un mod neobișnuit, atunci primul lucru pe care îl fac este să verific codul care depinde de blocare. Și de obicei găsesc probleme în ea.

Ambele interpretări sunt corecte.

Scrierea codului cu mai multe fire este ușoară. Dar este foarte, foarte dificil să folosiți corect primitivele de sincronizare. Poate că nu sunteți calificat să utilizați corect o singură încuietoare. La urma urmei, blocările și alte primitive de sincronizare sunt construcții care sunt ridicate la nivelul întregului sistem. Oamenii care înțeleg programarea paralelă mult mai bine decât dvs. folosesc aceste primitive pentru a construi structuri de date concurente și construcții de sincronizare la nivel înalt. Și tu și cu mine, programatori obișnuiți, luăm astfel de construcții și le folosim în codul nostru. Un programator de aplicații nu ar trebui să utilizeze primitive de sincronizare de nivel scăzut mai des decât efectuează apeluri directe către driverele de dispozitiv. Adică aproape niciodată.

Încercarea de a utiliza încuietori pentru rezolvarea problemelor de partajare a datelor este ca și cum ai stinge un foc cu oxigen lichid. La fel ca un incendiu, astfel de probleme sunt mai ușor de prevenit decât de remediat. Dacă scăpați de starea partajată, nu trebuie să abuzați nici de primitivele de sincronizare.

Majoritatea a ceea ce știți despre multithreading este irelevant

În tutorialele multithreading pentru începători, veți afla ce fire sunt. Apoi, autorul va începe să ia în considerare diferite moduri în care aceste fire pot funcționa în paralel - de exemplu, vorbește despre controlul accesului la date partajate folosind blocaje și semafore, să se gândească la ce lucruri se pot întâmpla atunci când se lucrează cu evenimente. Vom analiza cu atenție variabilele de stare, barierele de memorie, secțiunile critice, mutele, câmpurile volatile și operațiile atomice. Vor fi discutate exemple despre cum să utilizați aceste construcții de nivel scăzut pentru a efectua tot felul de operațiuni de sistem. După ce a citit acest material la jumătate, programatorul decide că știe deja suficient despre toate aceste primitive și despre utilizarea lor. La urma urmei, dacă știu cum funcționează acest lucru la nivel de sistem, îl pot aplica la fel la nivelul aplicației. Da?

Imaginați-vă că îi spuneți unui adolescent cum să asambleze singur un motor cu ardere internă. Apoi, fără nici un fel de antrenament în conducere, îl pui la volanul unei mașini și spui: „Du-te!” Adolescentul înțelege cum funcționează o mașină, dar nu are nicio idee cum să ajungă de la punctul A la punctul B pe el.

Înțelegerea modului în care funcționează firele la nivelul sistemului nu ajută în niciun fel la nivelul aplicației. Nu sugerez că programatorii nu trebuie să învețe toate aceste detalii de nivel scăzut. Doar nu vă așteptați să puteți aplica aceste cunoștințe chiar atunci când proiectați sau dezvoltați o aplicație de afaceri.

Literatura introductivă de filetare (și cursurile academice conexe) nu ar trebui să exploreze astfel de constructe de nivel scăzut. Trebuie să vă concentrați asupra rezolvării celor mai frecvente clase de probleme și să arătați dezvoltatorilor modul în care aceste probleme sunt rezolvate folosind capabilități la nivel înalt. În principiu, majoritatea aplicațiilor de afaceri sunt programe extrem de simple. Citesc date de la unul sau mai multe dispozitive de intrare, efectuează o prelucrare complexă a acestor date (de exemplu, în proces, solicită mai multe date) și apoi produc rezultatele.

Adesea, astfel de programe se încadrează perfect în modelul furnizor-consumator, care necesită doar trei fire:

  • fluxul de intrare citește datele și le pune în coada de intrare;
  • un fir de lucru citește înregistrări din coada de intrare, le procesează și pune rezultatele în coada de ieșire;
  • fluxul de ieșire citește intrările din coada de ieșire și le stochează.

Aceste trei fire funcționează independent, comunicarea dintre ele are loc la nivelul cozii.

În timp ce din punct de vedere tehnic aceste cozi pot fi considerate ca zone de stare partajată, în practică ele sunt doar canale de comunicare în care operează propria lor sincronizare internă. Cozile acceptă lucrul simultan cu mulți producători și consumatori, puteți adăuga și elimina articole din ele în paralel.

Deoarece etapele de intrare, procesare și ieșire sunt izolate unele de altele, implementarea lor poate fi ușor modificată fără a afecta restul programului. Atâta timp cât tipul de date din coadă nu se modifică, puteți refactoriza componentele individuale ale programului la discreția dvs. În plus, întrucât un număr arbitrar de furnizori și consumatori participă la coadă, nu este dificil să adăugați alți producători / consumatori. Putem avea zeci de fluxuri de intrare care scriu informații pe aceeași coadă sau zeci de fire de lucru care preiau informații din coada de intrare și digerează date. În cadrul unui singur computer, un astfel de model se potrivește bine.

Cel mai important, limbajele și bibliotecile de programare moderne facilitează crearea de aplicații producător-consumator. În .NET, veți găsi Colecții paralele și biblioteca de flux de date TPL. Java are serviciul Executor, precum și BlockingQueue și alte clase din spațiul de nume java.util.concurrent. C ++ are o bibliotecă de threading Boost și biblioteca Thread Building Blocks de la Intel. Microsoft Visual Studio 2013 introduce agenți asincroni. Biblioteci similare sunt disponibile și în Python, JavaScript, Ruby, PHP și, din câte știu eu, în multe alte limbi. Puteți crea o aplicație producător-consumator folosind oricare dintre aceste pachete, fără a fi nevoie să recurgeți vreodată la blocări, semafore, variabile de condiție sau orice alte primitive de sincronizare.

O mare varietate de primitive de sincronizare sunt utilizate în mod liber în aceste biblioteci. Este în regulă. Toate aceste biblioteci sunt scrise de oameni care înțeleg multithreading-ul incomparabil mai bine decât programatorul mediu. Lucrul cu o astfel de bibliotecă este practic același lucru cu utilizarea unei biblioteci de limbă de rulare. Acest lucru poate fi comparat cu programarea într-un limbaj la nivel înalt, mai degrabă decât cu limbajul de asamblare.

Modelul furnizor-consumator este doar unul dintre numeroasele exemple. Bibliotecile de mai sus conțin clase care pot fi utilizate pentru a implementa multe dintre modelele comune de proiectare a filetării fără a intra în detalii de nivel scăzut. Este posibil să creați aplicații multithread pe scară largă fără să vă faceți griji cu privire la modul în care firele sunt coordonate și sincronizate.

Lucrați cu bibliotecile

Deci, crearea de programe multi-thread nu este fundamental diferită de scrierea de programe sincrone single-thread. Principiile importante ale încapsulării și ascunderii datelor sunt universale și cresc în importanță doar atunci când sunt implicate mai multe fire concurente. Dacă neglijați aceste aspecte importante, atunci nici cea mai cuprinzătoare cunoaștere a filetării la nivel scăzut nu vă va salva.

Dezvoltatorii moderni trebuie să rezolve o mulțime de probleme la nivelul programării aplicațiilor, se întâmplă că pur și simplu nu există timp să ne gândim la ceea ce se întâmplă la nivel de sistem. Cu cât aplicațiile sunt mai complicate, cu atât detaliile mai complexe trebuie ascunse între nivelurile API. Facem acest lucru de mai bine de o duzină de ani. Se poate argumenta că ascunderea calitativă a complexității sistemului de la programator este principalul motiv pentru care programatorul este capabil să scrie aplicații moderne. De altfel, nu ascundem complexitatea sistemului prin implementarea buclei de mesaje UI, construirea protocoalelor de comunicație la nivel scăzut etc.?

Situația este similară cu multithreading. Majoritatea scenariilor cu mai multe fire pe care programatorul mediu de aplicații de afaceri le-ar putea întâlni sunt deja bine cunoscute și bine implementate în biblioteci. Funcțiile bibliotecii fac o treabă excelentă de a ascunde complexitatea copleșitoare a paralelismului. Trebuie să învățați cum să utilizați aceste biblioteci în același mod în care utilizați bibliotecile de elemente ale interfeței cu utilizatorul, protocoale de comunicații și numeroase alte instrumente care funcționează doar. Lăsați multithread-ul de nivel scăzut pe seama specialiștilor - autorilor bibliotecilor utilizate în crearea aplicațiilor.

NS Acest articol nu este destinat domorilor experimentați de Python, pentru care dezlegarea acestei mingi de șerpi este jocul copiilor, ci mai degrabă o imagine de ansamblu superficială a capacităților multithreading pentru pythonul nou dependent.

Din păcate, nu există atât de mult material în limba rusă pe tema multithreading-ului în Python, iar pythonerii care nu auziseră nimic, de exemplu, despre GIL, au început să vină la mine cu o regularitate de invidiat. În acest articol voi încerca să descriu cele mai de bază caracteristici ale pythonului multithreaded, să vă spun ce este GIL și cum să trăiți cu el (sau fără el) și multe altele.


Python este un limbaj de programare fermecător. Combină perfect multe paradigme de programare. Majoritatea sarcinilor pe care le poate îndeplini un programator sunt rezolvate aici ușor, elegant și concis. Dar pentru toate aceste probleme, o soluție cu un singur fir este adesea suficientă, iar programele cu un singur fir sunt de obicei previzibile și ușor de depanat. Nu același lucru se poate spune despre programele cu mai multe fire și multiprocesare.

Aplicații cu mai multe fire


Python are un modul filetat și are tot ce aveți nevoie pentru programarea multi-thread: există diferite tipuri de blocări și un semafor și un mecanism de evenimente. Într-un cuvânt - tot ce este necesar pentru marea majoritate a programelor cu mai multe fire. Mai mult decât atât, utilizarea tuturor acestor instrumente este destul de simplă. Să luăm în considerare un exemplu de program care pornește 2 fire. Un fir scrie zece "0", celălalt - zece "1" și strict pe rând.

filetare de import

scriitor def

pentru i în xrange (10):

tipărește x

Event_for_set.set ()

# evenimente init

e1 = threading.Event ()

e2 = threading.Event ()

# fire de inițiere

0, e1, e2))

1, e2, e1))

# începeți firele

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Fără cod magic sau voodoo. Codul este clar și consecvent. Mai mult, după cum puteți vedea, am creat un flux dintr-o funcție. Acest lucru este foarte convenabil pentru sarcini mici. Acest cod este, de asemenea, destul de flexibil. Să presupunem că avem un al treilea proces care scrie „2”, atunci codul va arăta astfel:

filetare de import

scriitor def (x, event_for_wait, event_for_set):

pentru i în xrange (10):

Event_for_wait.wait () # așteptați evenimentul

Event_for_wait.clear () # eveniment curat pentru viitor

tipărește x

Event_for_set.set () # set eveniment pentru firul vecin

# evenimente init

e1 = threading.Event ()

e2 = threading.Event ()

e3 = threading.Event ()

# fire de inițiere

t1 = threading.Thread (target = writer, args = ( 0, e1, e2))

t2 = threading.Tread (target = writer, args = ( 1, e2, e3))

t3 = threading.Thread (target = writer, args = ( 2, e3, e1))

# începeți firele

t1.start ()

t2.start ()

t3.start ()

e1.set () # inițiază primul eveniment

# uniți firele la firul principal

t1.join ()

t2.join ()

t3.join ()


Am adăugat un nou eveniment, un fir nou și am schimbat ușor parametrii cu care
fluxurile încep (desigur, puteți scrie o soluție mai generală utilizând, de exemplu, MapReduce, dar acest lucru nu depășește scopul acestui articol).
După cum puteți vedea, nu există încă magie. Totul este simplu și direct. Să mergem mai departe.

Global Interpreter Lock


Există două motive cele mai frecvente pentru a utiliza fire: în primul rând, pentru a crește eficiența utilizării arhitecturii multicore a procesoarelor moderne și, prin urmare, a performanței programului;
în al doilea rând, dacă trebuie să împărțim logica programului în secțiuni paralele, complet sau parțial asincrone (de exemplu, pentru a putea face ping mai multe servere în același timp).

În primul caz, ne confruntăm cu o astfel de limitare a Python (sau mai degrabă implementarea sa principală CPython) ca Global Interpreter Lock (sau GIL pe scurt). Conceptul GIL este acela că un singur fir poate fi executat de un procesor la un moment dat. Acest lucru se face astfel încât să nu existe nici o luptă între fire pentru variabile separate. Firul executabil câștigă acces la întregul mediu. Această caracteristică a implementării firului în Python simplifică foarte mult lucrul cu fire și oferă o anumită siguranță a firelor.

Dar există un punct subtil: poate părea că o aplicație multi-thread va rula exact aceeași perioadă de timp ca o aplicație single-thread care face același lucru sau suma timpului de execuție al fiecărui thread de pe CPU. Dar aici ne așteaptă un efect neplăcut. Luați în considerare programul:

cu open ("test1.txt", "w") ca fout:

pentru i în xrange (1000000):

print >> fout, 1


Acest program scrie doar un milion de linii „1” într-un fișier și îl face în ~ 0,35 secunde pe computerul meu.

Luați în considerare un alt program:

din threading import Thread

scriitor def (nume de fișier, n):

cu deschis (nume de fișier, "w") ca fout:

pentru i în xrange (n):

print >> fout, 1

t1 = Fir (țintă = scriitor, args = („test2.txt”, 500000,))

t2 = thread (target = writer, args = („test3.txt”, 500000,))

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Acest program creează 2 fire. În fiecare fir, scrie într-un fișier separat, jumătate de milion de rânduri „1”. De fapt, cantitatea de muncă este aceeași ca în programul anterior. Dar, în timp, aici se obține un efect interesant. Programul poate rula de la 0,7 secunde până la 7 secunde. De ce se întâmplă asta?

Acest lucru se datorează faptului că atunci când un thread nu are nevoie de o resursă CPU, eliberează GIL și, în acest moment, poate încerca să îl obțină, și un alt thread, precum și firul principal. În același timp, sistemul de operare, știind că există multe nuclee, poate agrava totul încercând să distribuie fire între nuclee.

UPD: în acest moment, în Python 3.2, există o implementare îmbunătățită a GIL, în care această problemă este rezolvată parțial, în special datorită faptului că fiecare fir, după ce a pierdut controlul, așteaptă o scurtă perioadă de timp înainte de aceasta poate captura din nou GIL (există o prezentare bună în limba engleză)

„Așadar, nu puteți scrie programe multithread eficiente în Python?” Întrebați. Nu, desigur, există o cale de ieșire și chiar mai multe.

Aplicații multiprocesare


Pentru a rezolva într-un anumit sens problema descrisă în paragraful anterior, Python are un modul subproces ... Putem scrie un program pe care dorim să îl executăm într-un fir paralel (de fapt, deja un proces). Și rulați-l într-unul sau mai multe fire într-un alt program. Acest lucru ar accelera cu adevărat programul nostru, deoarece firele create în lansatorul GIL nu se ridică, ci doar așteaptă finalizarea procesului de rulare. Cu toate acestea, această metodă are multe probleme. Principala problemă este că devine dificilă transferarea datelor între procese. Trebuie să serializați cumva obiecte, să stabiliți comunicarea prin PIPE sau alte instrumente, dar toate acestea sunt inevitabile și codul devine dificil de înțeles.

O altă abordare ne poate ajuta aici. Python are un modul multiprocesare ... În ceea ce privește funcționalitatea, acest modul seamănă filetat ... De exemplu, procesele pot fi create în același mod din funcții obișnuite. Metodele de lucru cu procese sunt aproape aceleași ca și pentru firele din modulul de filetare. Dar pentru sincronizarea proceselor și schimbul de date, este obișnuit să se utilizeze alte instrumente. Vorbim despre cozi (Coadă) și conducte (Pipe). Cu toate acestea, analogii de blocări, evenimente și semafore care erau în filetare sunt, de asemenea, aici.

În plus, modulul multiprocesare are un mecanism pentru lucrul cu memoria partajată. Pentru aceasta, modulul are clase de variabilă (valoare) și matrice (matrice), care pot fi „partajate” între procese. Pentru comoditatea de a lucra cu variabile partajate, puteți utiliza clasele de manager. Sunt mai flexibile și mai ușor de utilizat, dar mai încet. Trebuie remarcat faptul că există o oportunitate plăcută de a crea tipuri comune din modulul ctypes utilizând modulul multiprocessing.sharedctypes.

De asemenea, în modulul multiprocesare există un mecanism de creare a grupurilor de procese. Acest mecanism este foarte convenabil de utilizat pentru implementarea modelului Master-Worker sau pentru implementarea unei Hărți paralele (care într-un anumit sens este un caz special al Master-Worker).

Dintre principalele probleme legate de lucrul cu modulul multiprocesare, merită menționată dependența relativă de platformă a acestui modul. Deoarece lucrul cu procesele este organizat diferit în diferite sisteme de operare, unele restricții sunt impuse codului. De exemplu, Windows nu are un mecanism de furcă, astfel încât punctul de separare a procesului trebuie să fie înfășurat în:

dacă __name__ == "__main__":


Cu toate acestea, acest design este deja o formă bună.

Ce altceva...


Există alte biblioteci și abordări pentru scrierea aplicațiilor paralele în Python. De exemplu, puteți utiliza Hadoop + Python sau diferite implementări Python MPI (pyMPI, mpi4py). Puteți folosi chiar împachetări de biblioteci existente C ++ sau Fortran. Aici se pot menționa astfel de cadre / biblioteci precum Pyro, Twisted, Tornado și multe altele. Dar toate acestea sunt deja dincolo de scopul acestui articol.

Dacă ți-a plăcut stilul meu, atunci în articolul următor voi încerca să-ți spun cum să scrii interpreți simpli în PLY și pentru ce pot fi folosite.

Capitolul 10.

Aplicații cu mai multe fire

Multitasking-ul în sistemele de operare moderne este considerat de la sine [ Înainte de apariția Apple OS X, pe computerele Macintosh nu existau sisteme de operare moderne multitasking. Este foarte dificil să proiectezi corect un sistem de operare cu multitasking complet, astfel încât OS X trebuia să se bazeze pe Unix.]. Utilizatorul se așteaptă ca atunci când editorul de text și clientul de e-mail sunt lansate în același timp, aceste programe nu vor intra în conflict, iar la primirea e-mail-ului, editorul nu va înceta să funcționeze. Când mai multe programe sunt lansate în același timp, sistemul de operare comută rapid între programe, oferindu-le un procesor la rândul său (cu excepția cazului în care, desigur, sunt instalate mai multe procesoare pe computer). Ca urmare, iluzie rularea mai multor programe în același timp, deoarece chiar și cel mai bun dactilograf (și cea mai rapidă conexiune la Internet) nu poate ține pasul cu un procesor modern.

Multithreading-ul, într-un anumit sens, poate fi văzut ca următorul nivel de multitasking: în loc să comute între diferite programe, sistemul de operare comută între diferite părți ale aceluiași program. De exemplu, un client de e-mail cu mai multe fire vă permite să primiți mesaje de e-mail noi în timp ce citiți sau compuneți mesaje noi. În zilele noastre, multithreading este, de asemenea, considerat de la sine acordat de mulți utilizatori.

VB nu a avut niciodată suport normal multithreading. Este adevărat, una dintre soiurile sale a apărut în VB5 - model de streaming colaborativ(filetarea apartamentului). După cum veți vedea în scurt timp, modelul colaborativ oferă programatorului unele dintre avantajele multithreading-ului, dar nu profită din plin de toate caracteristicile. Mai devreme sau mai târziu, trebuie să treceți de la o mașină de antrenament la una reală, iar VB .NET a devenit prima versiune a VB cu suport pentru un model multithread gratuit.

Cu toate acestea, multithreading-ul nu este una dintre caracteristicile care sunt ușor de implementat în limbaje de programare și ușor de stăpânit de programatori. De ce?

Deoarece în aplicațiile cu mai multe fire, pot apărea erori foarte complicate care apar și dispar imprevizibil (iar astfel de erori sunt cele mai dificil de depanat).

Avertisment sincer: multithreading este unul dintre cele mai dificile domenii de programare. Cea mai mică neatenție duce la apariția unor erori evazive, a căror corecție ia sume astronomice. Din acest motiv, acest capitol conține multe rău exemple - le-am scris în mod deliberat în așa fel încât să demonstreze erori comune. Aceasta este cea mai sigură abordare a învățării programării cu mai multe fire: trebuie să puteți identifica potențialele probleme atunci când totul pare să funcționeze bine la prima vedere și să știți cum să le rezolvați. Dacă doriți să utilizați tehnici de programare multi-thread, nu puteți face fără ea.

Acest capitol va pune o bază solidă pentru alte lucrări independente, dar nu vom putea descrie programarea multithread în toate complexitățile - doar documentația tipărită din clasele spațiului de nume Threading durează mai mult de 100 de pagini. Dacă doriți să stăpâniți programarea multithread la un nivel superior, consultați cărțile specializate.

Dar, oricât de periculoasă este programarea multithreaded, este indispensabilă pentru soluționarea profesională a unor probleme. Dacă programele dvs. nu folosesc multithreading acolo unde este cazul, utilizatorii vor deveni foarte frustrați și vor prefera un alt produs. De exemplu, numai în cea de-a patra versiune a popularului program de e-mail Eudora au apărut capabilități multithread, fără de care este imposibil să ne imaginăm vreun program modern pentru lucrul cu e-mail. În momentul în care Eudora a introdus suportul multi-threading, mulți utilizatori (inclusiv unul dintre autorii acestei cărți) au trecut la alte produse.

În cele din urmă, în .NET, programele cu un singur fir pur și simplu nu există. Tot Programele .NET sunt multithread deoarece colectorul de gunoi rulează ca un proces de fundal cu prioritate redusă. Așa cum se arată mai jos, pentru o programare grafică serioasă în .NET, filetarea corectă poate ajuta la prevenirea blocării interfeței grafice atunci când programul execută operații lungi.

Vă prezentăm multithreading

Fiecare program funcționează într-un anumit context, descriind distribuția codului și a datelor în memorie. Salvând contextul, starea fluxului programului este salvată, ceea ce vă permite să îl restaurați în viitor și să continuați executarea programului.

Salvarea contextului vine cu un cost în timp și memorie. Sistemul de operare își amintește starea firului de program și transferă controlul către alt fir. Când programul dorește să continue executarea firului suspendat, contextul salvat trebuie restaurat, ceea ce durează și mai mult. Prin urmare, multithreading-ul trebuie utilizat numai atunci când beneficiile compensează toate costurile. Câteva exemple tipice sunt enumerate mai jos.

  • Funcționalitatea programului este împărțită în mod clar și natural în mai multe operațiuni eterogene, ca în exemplul cu primirea de e-mail și pregătirea mesajelor noi.
  • Programul efectuează calcule lungi și complexe și nu doriți ca interfața grafică să fie blocată pe durata calculelor.
  • Programul rulează pe un computer multiprocesor cu un sistem de operare care acceptă utilizarea mai multor procesoare (atâta timp cât numărul de fire active nu depășește numărul de procesoare, executarea paralelă este practic lipsită de costurile asociate cu comutarea firelor).

Înainte de a trece la mecanica programelor multithread, este necesar să subliniem o circumstanță care adesea provoacă confuzie între începători în domeniul programării multithread.

O procedură, nu un obiect, este executată în fluxul de programe.

Este dificil de spus ce se înțelege prin expresia „obiectul execută”, dar unul dintre autori predă adesea seminarii despre programarea multithreading și această întrebare este pusă mai des decât altele. Poate cineva crede că lucrarea firului de program începe cu un apel la metoda Nouă a clasei, după care firul procesează toate mesajele transmise către obiectul corespunzător. Astfel de reprezentări absolut sunt greșite. Un obiect poate conține mai multe fire care execută metode diferite (și uneori chiar aceleași), în timp ce mesajele obiectului sunt transmise și primite de mai multe fire diferite (apropo, acesta este unul dintre motivele care complică programarea multi-thread: pentru a depana un program, trebuie să aflați ce fir într-un moment dat efectuează una sau alta procedură!).

Deoarece firele sunt create din metode de obiecte, obiectul în sine este de obicei creat înainte de fir. După crearea cu succes a obiectului, programul creează un fir, îi transmite adresa metodei obiectului și abia după aceea dă ordinul de a începe executarea firului. Procedura pentru care a fost creat firul, ca toate procedurile, poate crea obiecte noi, poate efectua operațiuni pe obiecte existente și poate apela alte proceduri și funcții care se află în sfera sa de aplicare.

Metodele obișnuite de clase pot fi, de asemenea, executate în fire de program. În acest caz, rețineți, de asemenea, o altă circumstanță importantă: firul se termină cu o ieșire din procedura pentru care a fost creat. Finalizarea normală a fluxului programului nu este posibilă până când procedura nu este încheiată.

Firele se pot termina nu numai în mod natural, ci și în mod anormal. În general, acest lucru nu este recomandat. Consultați Terminarea și întreruperea fluxurilor pentru mai multe informații.

Instrumentele de bază .NET legate de utilizarea firelor de program sunt concentrate în spațiul de nume Threading. Prin urmare, majoritatea programelor multithread ar trebui să înceapă cu următoarea linie:

Sistemul de importuri

Importarea unui spațiu de nume face programul mai ușor de tastat și activează tehnologia IntelliSense.

Legătura directă a fluxurilor cu procedurile sugerează că în această imagine, delegați(vezi capitolul 6). Mai exact, spațiul de nume Threading include delegatul ThreadStart, care este de obicei utilizat la pornirea firelor de program. Sintaxa pentru utilizarea acestui delegat arată astfel:

Sub delegat public ThreadStart ()

Codul apelat cu delegatul ThreadStart nu trebuie să aibă niciun parametru sau valoare de returnare, deci fire nu pot fi create pentru funcții (care returnează o valoare) și pentru proceduri cu parametri. Pentru a transfera informații din flux, trebuie să căutați și mijloace alternative, deoarece metodele executate nu returnează valori și nu pot utiliza transferul prin referință. De exemplu, dacă ThreadMethod se află în clasa WilluseThread, atunci ThreadMethod poate comunica informații modificând proprietățile instanțelor clasei WillUseThread.

Domenii de aplicații

Firele .NET rulează în așa-numitele domenii ale aplicației, definite în documentație ca „sandbox-ul în care rulează aplicația”. Un domeniu de aplicație poate fi considerat o versiune ușoară a proceselor Win32; un singur proces Win32 poate conține mai multe domenii de aplicații. Principala diferență între domeniile aplicației și procesele este că un proces Win32 are propriul spațiu de adrese (în documentație, domeniile aplicației sunt, de asemenea, comparate cu procesele logice care rulează într-un proces fizic). În NET, toată gestionarea memoriei este gestionată de runtime, astfel încât mai multe domenii de aplicații pot rula într-un singur proces Win32. Unul dintre beneficiile acestei scheme este capacitatea îmbunătățită de scalare a aplicațiilor. Instrumentele pentru lucrul cu domeniile aplicației fac parte din clasa AppDomain. Vă recomandăm să studiați documentația pentru această clasă. Cu ajutorul acestuia, puteți obține informații despre mediul în care rulează programul dvs. În special, clasa AppDomain este utilizată atunci când se reflectă asupra claselor de sistem .NET. Următorul program listează ansamblurile încărcate.

Sistemul de importuri. Reflexie

Modul Modulel

Sub Main ()

Reduceți domeniul ca AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

Pentru fiecare anasamblare în ansambluri

Console.WriteLinetanAssembly.Full Name) În continuare

Console.ReadLine ()

Sfârșitul Sub

Modul final

Crearea fluxurilor

Să începem cu un exemplu rudimentar. Să presupunem că doriți să rulați o procedură într-un fir separat care scade valoarea contorului într-o buclă infinită. Procedura este definită ca parte a clasei:

Clasa publică WillUseThreads

Public Sub SubtractFromCounter ()

Numărați ca număr întreg

Do While True count - = 1

Console.WriteLlne ("Sunt într-un alt fir și contor ="

& numara)

Buclă

Sfârșitul Sub

Clasa de sfârșit

Deoarece condiția buclă Do este întotdeauna adevărată, s-ar putea să credeți că nimic nu va interfera cu procedura SubtractFromCounter. Cu toate acestea, într-o aplicație cu mai multe fire, acest lucru nu este întotdeauna cazul.

Următorul fragment prezintă procedura Sub Main care pornește firul și comanda Importuri:

Opțiune strictă la sistemul de importuri. Modulul de filetare

Sub Main ()

1 Dim MyTest ca nou WillUseThreads ()

2 Dim bThreadStart Ca nou ThreadStart (AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread ca fir nou (bThreadStart)

4 "bThread.Start ()

Dim i Integer

5 Fă adevărat

Console.WriteLine ("În firul principal și numărul este" & i) i + = 1

Buclă

Sfârșitul Sub

Modul final

Să aruncăm o privire la cele mai importante puncte în ordine. În primul rând, procedura Sub Man n funcționează întotdeauna în fluxul principal(fir principal). În programele .NET, există întotdeauna cel puțin două fire care rulează: firul principal și firul de colectare a gunoiului. Linia 1 creează o nouă instanță a clasei de testare. În linia 2, creăm un delegat ThreadStart și trecem adresa procedurii SubtractFromCounter instanței clasei de test create în linia 1 (această procedură se numește fără parametri). BunPrin importarea spațiului de nume Threading, numele lung poate fi omis. Noul obiect thread este creat pe linia 3. Observați trecerea delegatului ThreadStart atunci când apelați constructorul clasei Thread. Unii programatori preferă să concateneze aceste două linii într-o singură linie logică:

Dim bThread As New Thread (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

În cele din urmă, linia 4 „pornește” firul apelând metoda Start a instanței Thread create pentru delegatul ThreadStart. Apelând această metodă, îi spunem sistemului de operare că procedura de scădere ar trebui să ruleze într-un fir separat.

Cuvântul „pornește” din paragraful anterior este inclus între ghilimele, deoarece aceasta este una dintre numeroasele ciudățenii programării multithread: Calling Start nu pornește firul! Îi spune doar sistemului de operare să programeze executarea firului specificat, dar este dincolo de controlul programului să pornească direct. Nu veți putea începe să executați thread-uri pe cont propriu, deoarece sistemul de operare controlează întotdeauna executarea thread-urilor. Într-o secțiune ulterioară, veți afla cum să utilizați prioritatea pentru a face ca sistemul de operare să pornească firul mai repede.

În fig. 10.1 arată un exemplu de ceea ce se poate întâmpla după pornirea unui program și apoi întreruperea acestuia cu tasta Ctrl + Break. În cazul nostru, un fir nou a început numai după ce contorul din firul principal a crescut la 341!

Orez. 10.1. Runtime de software simplu cu mai multe fire

Dacă programul rulează pentru o perioadă mai lungă de timp, rezultatul va arăta ceva asemănător celui prezentat în Fig. 10.2. Vedem că tufinalizarea firului de rulare este suspendată și controlul este transferat din nou la firul principal. În acest caz, există o manifestare multithread preventiv prin felierea timpului. Sensul acestui termen terifiant este explicat mai jos.

Orez. 10.2. Comutarea între fire într-un program simplu cu mai multe fire

Atunci când întrerupe firele și transferă controlul către alte fire, sistemul de operare folosește principiul multithreadului preventiv prin tranșarea timpului. Cuantificarea timpului rezolvă, de asemenea, una dintre problemele obișnuite care au apărut înainte în programele cu mai multe fire - un fir ocupă tot timpul CPU și nu este inferior controlului altor fire (de regulă, acest lucru se întâmplă în cicluri intensive ca cel de mai sus). Pentru a preveni deturnarea exclusivă a procesorului, firele dvs. ar trebui să transfere controlul către alte fire din când în când. Dacă programul se dovedește a fi „inconștient”, există o altă soluție, puțin mai puțin de dorit: sistemul de operare previne întotdeauna un fir de rulare, indiferent de nivelul său de prioritate, astfel încât accesul la procesor să fie acordat fiecărui fir de lucru din sistem.

Deoarece schemele de cuantificare ale tuturor versiunilor de Windows care rulează .NET au o porțiune de timp minimă pentru fiecare fir, în programarea .NET, problemele cu apucările exclusive de CPU nu sunt atât de grave. Pe de altă parte, dacă cadrul .NET este vreodată adaptat pentru alte sisteme, acest lucru se poate schimba.

Dacă includem următoarea linie în programul nostru înainte de a apela Start, atunci chiar și firele cu cea mai mică prioritate vor obține o fracțiune din timpul procesorului:

bThread.Priority = ThreadPriority.Highest

Orez. 10.3. Firul cu cea mai mare prioritate pornește de obicei mai repede

Orez. 10.4. Procesorul este, de asemenea, prevăzut pentru fire cu prioritate mai mică

Comanda atribuie prioritatea maximă noului fir și scade prioritatea firului principal. Smochin. 10.3 se poate observa că noul fir începe să funcționeze mai repede decât înainte, dar, așa cum se arată în Fig. 10.4, firul principal primește, de asemenea, controlullene (deși pentru un timp foarte scurt și numai după o muncă prelungită a fluxului cu scădere). Când rulați programul pe computerele dvs., veți obține rezultate similare cu cele prezentate în Fig. 10.3 și 10.4, dar din cauza diferențelor dintre sistemele noastre, nu va exista o potrivire exactă.

Tipul enumerat ThreadPrlority include valori pentru cinci niveluri de prioritate:

ThreadPriority.Hestest

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Metoda de alăturare

Uneori, un fir de program trebuie să fie întrerupt până la finalizarea altui fir. Să presupunem că doriți să întrerupeți firul 1 până când firul 2 își finalizează calculul. Pentru aceasta din fluxul 1 metoda Join este apelată pentru fluxul 2. Cu alte cuvinte, comanda

thread2.Alaturați-vă ()

suspendă firul curent și așteaptă finalizarea firului 2. Firul 1 merge la stat blocat.

Dacă vă alăturați fluxului 1 la fluxul 2 utilizând metoda Join, sistemul de operare va porni automat fluxul 1 după fluxul 2. Rețineți că procesul de pornire este nedeterminist: este imposibil să se spună exact cât timp după sfârșitul firului 2, firul 1 va începe să funcționeze. Există o altă versiune a Join care returnează o valoare booleană:

thread2.Alaturați-vă (întreg)

Această metodă fie așteaptă completarea firului 2, fie deblochează firul 1 după ce a trecut intervalul de timp specificat, determinând programatorul sistemului de operare să aloce din nou timpul CPU pentru fir. Metoda returnează True dacă firul 2 se termină înainte de expirarea intervalului de expirare specificat și False în caz contrar.

Amintiți-vă regula de bază: dacă firul 2 a fost finalizat sau a expirat, nu aveți control asupra când este activat firul 1.

Numele firelor, CurrentThread și ThreadState

Proprietatea Thread.CurrentThread returnează o referință la obiectul thread care se execută în prezent.

Deși există o fereastră de subiect minunată pentru depanarea aplicațiilor multithread în VB .NET, care este descrisă mai jos, am fost foarte des ajutați de comandă

MsgBox (Thread.CurrentThread.Name)

De multe ori s-a dovedit că codul era executat într-un fir complet diferit de cel care trebuia să fie executat.

Reamintim că termenul „planificare nedeterministă a fluxurilor de programe” înseamnă un lucru foarte simplu: programatorul nu are practic nici un mijloc la dispoziție pentru a influența munca planificatorului. Din acest motiv, programele folosesc adesea proprietatea ThreadState, care returnează informații despre starea curentă a unui thread.

Fereastra fluxurilor

Fereastra Threads din Visual Studio .NET este de neprețuit în depanarea programelor cu mai multe fire. Este activat de comanda Debug> submeniu Windows în modul întrerupere. Să presupunem că ați atribuit un nume firului bThread cu următoarea comandă:

bThread.Name = "Scăderea firului"

O vedere aproximativă a ferestrei fluxurilor după întreruperea programului cu combinația de taste Ctrl + Break (sau într-un alt mod) este prezentată în Fig. 10.5.

Orez. 10.5. Fereastra fluxurilor

Săgeata din prima coloană marchează firul activ returnat de proprietatea Thread.CurrentThread. Coloana ID conține ID-uri de fir numerice. Următoarea coloană listează numele fluxului (dacă este atribuit). Coloana Locație indică procedura de executat (de exemplu, procedura WriteLine a clasei Console din Figura 10.5). Coloanele rămase conțin informații despre prioritate și fire suspendate (vezi secțiunea următoare).

Fereastra thread (nu sistemul de operare!) Vă permite să controlați thread-urile programului dvs. folosind meniurile contextuale. De exemplu, puteți opri firul curent făcând clic dreapta pe linia corespunzătoare și alegând comanda Freeze (mai târziu, firul oprit poate fi reluat). Oprirea firelor este adesea utilizată la depanare pentru a preveni interferența unei aplicații cu un fir defect. În plus, fereastra de fluxuri vă permite să activați un alt flux (neoprit); pentru a face acest lucru, faceți clic dreapta pe linia necesară și selectați comanda Switch To Thread din meniul contextual (sau pur și simplu faceți dublu clic pe linia thread). După cum se va arăta mai jos, acest lucru este foarte util în diagnosticarea posibilelor blocaje.

Suspendarea unui flux

Fluxurile neutilizate temporar pot fi transferate într-o stare pasivă folosind metoda Slеer. Un flux pasiv este, de asemenea, considerat blocat. Desigur, atunci când un thread este pus într-o stare pasivă, restul de fire vor avea mai multe resurse de procesor. Sintaxa standard a metodei Slеer este după cum urmează: Thread.Sleep (interval_in_milliseconds)

Ca urmare a apelului Sleep, firul activ devine pasiv pentru cel puțin un număr specificat de milisecunde (cu toate acestea, activarea imediat după expirarea intervalului specificat nu este garantată). Vă rugăm să rețineți: la apelarea metodei, nu se trece la o referință la un fir specific - metoda Sleep este apelată numai pentru firul activ.

O altă versiune a Sleep face ca firul curent să renunțe la restul timpului CPU alocat:

Thread.Sleep (0)

Următoarea opțiune pune firul curent într-o stare pasivă pentru un timp nelimitat (activarea are loc numai atunci când apelați întreruperea):

Thread.Slеer (Timeout.Infinite)

Deoarece firele pasive (chiar și cu un timeout nelimitat) pot fi întrerupte de metoda de întrerupere, care duce la inițierea unui ThreadlnterruptExcepti cu excepția, apelul Slayer este întotdeauna închis într-un bloc Try-Catch, ca în următorul fragment:

Încerca

Thread.Sleep (200)

„Starea pasivă a firului a fost întreruptă

Prindeți ca excepție

„Alte excepții

Sfârșit Încercați

Fiecare program .NET rulează pe un fir de program, astfel încât metoda Sleep este de asemenea utilizată pentru a suspenda programele (dacă spațiul de nume Threadipg nu este importat de program, trebuie să utilizați numele complet calificat Threading.Thread. Sleep).

Închiderea sau întreruperea firelor de program

Un fir se va termina automat atunci când este creată metoda specificată atunci când este creat delegatul ThreadStart, dar uneori este necesar să se încheie metoda (și, prin urmare, firul) atunci când apar anumiți factori. În astfel de cazuri, fluxurile verifică de obicei variabilă condițională,în funcție de starea căruiase ia o decizie cu privire la o ieșire de urgență din flux. De obicei, o procedură Do-While este inclusă în procedura pentru aceasta:

Sub ThreadedMethod ()

„Programul trebuie să ofere mijloace pentru sondaj

"variabilă condițională.

"De exemplu, o variabilă condițională poate fi stilizată ca o proprietate

Do While conditionVariable = False And MoreWorkToDo

"Codul principal

Loop End Sub

Este nevoie de ceva timp pentru a examina variabila condițională. Ar trebui să utilizați interogarea persistentă într-o stare de buclă numai dacă așteptați ca firul să se termine prematur.

Dacă variabila de condiție trebuie verificată la o anumită locație, utilizați comanda If-Then împreună cu Exit Sub într-o buclă infinită.

Accesul la o variabilă condițională trebuie sincronizat, astfel încât expunerea din alte fire să nu interfereze cu utilizarea sa normală. Acest subiect important este tratat în secțiunea „Depanare: sincronizare”.

Din păcate, codul de fire pasive (sau altfel blocate) nu este executat, deci opțiunea cu interogarea unei variabile condiționale nu este potrivită pentru acestea. În acest caz, apelați metoda de întrerupere a variabilei obiect care conține o referință la firul dorit.

Metoda de întrerupere poate fi apelată numai pe fire în starea Wait, Sleep sau Join. Dacă apelați întreruperea pentru un fir care se află într-una din stările listate, atunci după un timp firul va începe să funcționeze din nou, iar mediul de execuție va iniția un ThreadlnterruptedExcepti cu excepția în fir. Acest lucru se întâmplă chiar dacă firul a fost făcut pasiv la infinit apelând Thread.Sleepdimeout. Infinit). Spunem „după un timp”, deoarece programarea firelor este nedeterministă. Excepția ThreadlnterruptedExcepti on este capturată de secțiunea Catch, care conține codul de ieșire din starea de așteptare. Cu toate acestea, secțiunea Catch nu trebuie să încheie firul într-un apel de întrerupere - firul gestionează excepția după cum consideră potrivit.

În .NET, metoda de întrerupere poate fi apelată chiar și pentru fire deblocate. În acest caz, firul este întrerupt la cea mai apropiată blocare.

Suspendarea și uciderea firelor

Spațiul de nume Threading conține alte metode care întrerup filetarea normală:

  • Suspenda;
  • Abortează.

Este greu de spus de ce .NET a inclus asistență pentru aceste metode - apelarea Suspend and Abort va provoca probabil ca programul să devină instabil. Niciuna dintre metode nu permite dezinitializarea normală a fluxului. În plus, când se apelează Suspend sau Abort, este imposibil să se prezică în ce stare firul va lăsa obiectele după ce a fost suspendat sau avortat.

Apelarea Abort aruncă o excepție ThreadAbortException. Pentru a vă ajuta să înțelegeți de ce această excepție ciudată nu ar trebui tratată în programe, iată un extras din documentația .NET SDK:

„... Când un fir este distrus apelând Abort, timpul de rulare lansează o excepție ThreadAbortException. Acesta este un tip special de excepție care nu poate fi prins de program. Când această excepție este aruncată, runtime rulează toate blocurile Final înainte de a termina firul. Deoarece orice acțiune poate avea loc în blocurile Final, apelați Join pentru a vă asigura că fluxul este distrus. "

Morală: Abort și Suspend nu sunt recomandate (și dacă tot nu puteți face fără Suspend, reluați firul suspendat folosind metoda Reluare). Puteți termina în siguranță un fir numai prin interogarea unei variabile de condiție sincronizate sau apelând metoda de întrerupere discutată mai sus.

Fire de fundal (demoni)

Unele fire care rulează în fundal se opresc automat când alte componente ale programului se opresc. În special, colectorul de gunoi rulează într-unul din firele de fundal. Firele de fundal sunt de obicei create pentru a primi date, dar acest lucru se face numai dacă alte fire rulează cod care poate procesa datele primite. Sintaxă: numele fluxului. IsBackGround = Adevărat

Dacă în aplicație rămân doar fire de fundal, aplicația se va termina automat.

Exemplu mai serios: extragerea datelor din codul HTML

Vă recomandăm să utilizați fluxuri numai atunci când funcționalitatea programului este clar împărțită în mai multe operații. Un bun exemplu este programul de extragere HTML din capitolul 9. Clasa noastră face două lucruri: preluarea datelor de pe Amazon și prelucrarea acestora. Acesta este un exemplu perfect de situație în care programarea multithread este cu adevărat adecvată. Creăm clase pentru mai multe cărți diferite și apoi analizăm datele în fluxuri diferite. Crearea unui fir nou pentru fiecare carte crește eficiența programului, deoarece în timp ce un fir primește date (care poate necesita așteptare pe serverul Amazon), un alt fir va fi ocupat cu procesarea datelor care au fost deja primite.

Versiunea multi-thread a acestui program funcționează mai eficient decât versiunea single-thread doar pe un computer cu mai multe procesoare sau dacă recepția de date suplimentare poate fi combinată eficient cu analiza lor.

După cum sa menționat mai sus, numai procedurile care nu au parametri pot fi rulate în fire, așa că va trebui să faceți modificări minore la program. Mai jos este procedura de bază, rescrisă pentru a exclude parametrii:

Public Sub FindRank ()

m_Rank = ScrapeAmazon ()

Console.WriteLine ("rangul" & m_Name & "Is" & GetRank)

Sfârșitul Sub

Deoarece nu vom putea folosi câmpul combinat pentru stocarea și recuperarea informațiilor (scrierea de programe multi-thread cu o interfață grafică este discutată în ultima secțiune a acestui capitol), programul stochează datele a patru cărți într-o matrice, a cărei definiție începe astfel:

Dim theBook (3.1) As String theBook (0.0) = "1893115992"

theBook (0.l) = "Programare VB .NET" "Etc.

Patru fluxuri sunt create în același ciclu în care sunt create obiecte AmazonRanker:

Pentru i = 0 până la 3

Încerca

theRanker = New AmazonRanker (theBook (i.0). theBookd.1))

aThreadStart = New ThreadStar (AddressOf theRanker.FindRan ()

aThread = Subiect nou (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start () Prinde e ca excepție

Console.WriteLine (e.Message)

Sfârșit Încercați

Următorul

Mai jos este textul complet al programului:

Opțiune strictă la importurile System.IO Imports System.Net

Sistemul de importuri

Modul Modulel

Sub Main ()

Atenuați cartea (3.1) ca șir

theBook (0.0) = "1893115992"

theBook (0.l) = "Programare VB .NET"

theBook (l.0) = "1893115291"

theBook (l.l) = "Programarea bazei de date VB .NET"

theBook (2,0) = "1893115623"

theBook (2.1) = Introducerea programatorului în C #. "

theBook (3.0) = "1893115593"

theBook (3.1) = "Gland platforma .Net"

Dim i Integer

Dim theRanker As = AmazonRanker

Dim aThreadStart As Threading.ThreadStart

Dim aThread As Threading.Thread

Pentru i = 0 până la 3

Încerca

theRanker = New AmazonRankerttheBook (i.0). theBook (i.1))

aThreadStart = New ThreadStart (AddressOf theRanker. FindRank)

aThread = Subiect nou (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start ()

Prindeți ca excepție

Console.WriteLlnete.Message)

Sfârșit Încercați Următorul

Console.ReadLine ()

Sfârșitul Sub

Modul final

Clasa publică AmazonRanker

Private m_URL As String

Private m_Rank As Integer

Private m_Name As String

Public Sub New (ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = theName End Sub

Public Sub FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline („rangul„ & m_Name & „este„

& GetRank) End Sub

Proprietatea de citire publică GetRank () ca șir Get

Dacă m_Rank<>0 Atunci

Returnează CStr (m_Rank) Altfel

" Probleme

End If

End Get

Proprietate finală

Proprietatea de citire publică GetName () ca șir Get

Returnează m_Name

End Get

Proprietate finală

Funcție privată ScrapeAmazon () În timp ce Încercați întreg

Dim theURL as New Uri (m_URL)

Reduceți cererea Ca cerere web

theRequest = WebRequest.Create (theURL)

Reduceți răspunsul ca răspuns Web

theResponse = theRequest.GetResponse

Dim aReader Ca nou StreamReader (theResponse.GetResponseStream ())

Reduceți datele ca șir

theData = aReader.ReadToEnd

Return Analyze (theData)

Prindeți E ca excepție

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

Consolă. Citeste linia ()

Încercare Încercare Funcție de încheiere

Analiza funcției private (ByVal theData as String) ca întreg

Dim Location As.Integer Location = theData.IndexOf (" Amazon.com

Rang de vânzări:") _

+ "Clasamentul vânzărilor Amazon.com:".Lungime

Dim temp As String

Faceți până laData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Location + = 1 Buclă

Returnează Clnt (temp)

Funcția de sfârșit

Clasa de sfârșit

Operațiile cu mai multe fire sunt utilizate în mod obișnuit în spațiile de nume .NET și I / O, astfel încât biblioteca .NET Framework oferă metode asincrone speciale pentru acestea. Pentru mai multe informații despre utilizarea metodelor asincrone atunci când scrieți programe cu mai multe fire, consultați metodele BeginGetResponse și EndGetResponse din clasa HTTPWebRequest.

Pericol principal (date generale)

Până în prezent, a fost luat în considerare singurul caz de utilizare sigur pentru fire - fluxurile noastre nu au modificat datele generale. Dacă permiteți modificarea datelor generale, erorile potențiale încep să se înmulțească exponențial și devine mult mai dificil să scăpați de ele pentru program. Pe de altă parte, dacă interziceți modificarea datelor partajate de diferite fire, programarea .NET multithreading nu va diferi cu greu de capacitățile limitate ale VB6.

Am dori să vă atragem atenția asupra unui mic program care demonstrează problemele care apar fără a intra în detalii inutile. Acest program simulează o casă cu un termostat în fiecare cameră. Dacă temperatura este cu 5 grade Fahrenheit sau mai mult (aproximativ 2,77 grade Celsius) mai mică decât temperatura țintă, comandăm sistemului de încălzire să crească temperatura cu 5 grade; în caz contrar, temperatura crește cu doar 1 grad. Dacă temperatura curentă este mai mare sau egală cu cea setată, nu se face nicio modificare. Controlul temperaturii în fiecare cameră se efectuează cu un debit separat cu o întârziere de 200 de milisecunde. Lucrarea principală se face cu următorul fragment:

Dacă mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep (200)

Prinde cravată ca ThreadlnterruptedException

„Așteptarea pasivă a fost întreruptă

Prindeți ca excepție

"Alte excepții de încercare finală

mHouse.HouseTemp + - 5 "Etc.

Mai jos este codul sursă complet al programului. Rezultatul este prezentat în Fig. 10.6: Temperatura din casă a ajuns la 105 grade Fahrenheit (40,5 grade Celsius)!

1 Opțiune Strict On

2 Sistem de import. Filetare

3 Modul Modul

4 Sub Main ()

5 Dim myHouse As New House (l0)

6 Consolă. Citeste linia ()

7 Sfârșitul Sub

8 Modul final

9 Casă publică

10 Public Const MAX_TEMP As Integer = 75

11 Private mCurTemp As Integer = 55

12 camere private () ca cameră

13 Public Sub New (ByVal numOfRooms As Inger)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart As Threading.ThreadStart

17 Reduceți firul ca fir

18 Pentru i = 0 To numOfRooms -1

19 Încearcă

20 mRooms (i) = NewRoom (Me, mCurTemp, CStr (i) & "throom")

21 aThreadStart - New ThreadStart (AddressOf _

mRooms (i) .CheckTempInRoom)

22 aThread = Subiect nou (aThreadStart)

23 aThread.Start ()

24 Captură E ca excepție

25 Console.WriteLine (E.StackTrace)

26 Sfârșit Încercați

27 În continuare

28 Sfârșitul Sub

29 Proprietate publică HouseTemp () Ca întreg

treizeci. obține

31 Returnează mCurTemp

32 Sfârșit Obține

33 Set (ByVal Value As Inger)

34 mCurTemp = Valoarea 35 Set final

36 Proprietate finală

37 Clasa de sfârșit

38 Cameră publică

39 Privat mCurTemp ca întreg

40 Private mName As String

41 Casă privată ca casă

42 Public Sub New (ByVal the House As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = theHouse

44 mCurTemp = temp

45 mName = roomName

46 Sfârșitul Sub

47 Public Sub CheckTempInRoom ()

48 ChangeTemperature ()

49 Sfârșitul Sub

50 Private Sub ChangeTemperature ()

51 Încearcă

52 Dacă mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 fire.Dorm (200)

54 mHouse.HouseTemp + - 5

55 Console.WriteLine („Sunt în” & Me.mName & _

56 ". Temperatura actuală este" & mHouse.HouseTemp)

57. El însuși mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 fir.Dorm (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Sunt în" & Me.mName & _

61 ". Temperatura actuală este" & mHouse.HouseTemp)

62 Altfel

63 Console.WriteLine („Sunt în” & Me.mName & _

64 ". Temperatura actuală este" & mHouse.HouseTemp)

65 "Nu faceți nimic, temperatura este normală

66 Sfârșit Dacă

67 Catch tae As ThreadlnterruptedException

68 "Așteptarea pasivă a fost întreruptă

69 Prindeți ca excepție

70 "Alte excepții

71 Sfârșit Încercați

72 Sfârșitul Sub

73 Clasa de sfârșit

Orez. 10.6. Probleme cu multithreading

Procedura Sub Main (rândurile 4-7) creează o „casă” cu zece „camere”. Clasa House stabilește o temperatură maximă de 75 grade Fahrenheit (aproximativ 24 de grade Celsius). Liniile 13-28 definesc un constructor de case destul de complex. Cheia pentru înțelegerea programului sunt liniile 18-27. Linia 20 creează un alt obiect de cameră și o trimitere la obiectul casei este transmisă constructorului, astfel încât obiectul camerei să poată face referire la el, dacă este necesar. Liniile 21-23 pornesc zece fluxuri pentru a regla temperatura din fiecare cameră. Clasa Room este definită pe liniile 38-73. Casă de referință coxpaeste stocat în variabila mHouse în constructorul clasei Room (linia 43). Codul pentru verificarea și reglarea temperaturii (liniile 50-66) arată simplu și natural, dar după cum veți vedea în curând, această impresie este înșelătoare! Rețineți că acest cod este înfășurat într-un bloc Try-Catch, deoarece programul folosește metoda Sleep.

Aproape nimeni nu ar fi de acord să trăiască la temperaturi de 105 grade Fahrenheit (40,5 până la 24 de grade Celsius). Ce s-a întâmplat? Problema este legată de următoarea linie:

Dacă mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Și se întâmplă următoarele: mai întâi, temperatura este verificată prin fluxul 1. El vede că temperatura este prea scăzută și o crește cu 5 grade. Din păcate, înainte ca temperatura să crească, fluxul 1 este întrerupt și controlul este transferat în fluxul 2. Fluxul 2 verifică aceeași variabilă nu a fost încă modificat fluxul 1. Astfel, fluxul 2 se pregătește, de asemenea, să ridice temperatura cu 5 grade, dar nu are timp să facă acest lucru și intră, de asemenea, într-o stare de așteptare. Procesul continuă până când fluxul 1 este activat și trece la următoarea comandă - creșterea temperaturii cu 5 grade. Creșterea se repetă atunci când toate cele 10 fluxuri sunt activate, iar locuitorii casei se vor simți prost.

Soluție la problemă: sincronizare

În programul anterior, apare o situație când ieșirea programului depinde de ordinea de execuție a firelor. Pentru a scăpa de el, trebuie să vă asigurați că comenzile de genul

Dacă mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

sunt procesate complet de firul activ înainte de a fi întrerupt. Această proprietate se numește rusine atomica - un bloc de cod trebuie executat de fiecare fir fără întrerupere, ca unitate atomică. Un grup de comenzi, combinate într-un bloc atomic, nu pot fi întrerupte de programatorul de fire până când nu este finalizat. Orice limbaj de programare multithread are propriile sale modalități de a asigura atomicitatea. În VB .NET, cel mai simplu mod de a utiliza comanda SyncLock este de a trece într-o variabilă obiect atunci când este apelat. Efectuați mici modificări la procedura ChangeTemperature din exemplul anterior, iar programul va funcționa bine:

Private Sub ChangeTemperature () SyncLock (mHouse)

Încerca

Dacă mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine („Sunt în” & Me.mName & _

„.Temperatura actuală este„ & mHouse.HouseTemp)

Însuși

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep (200) mHouse.HouseTemp + = 1

Console.WriteLine ("Sunt în" & Me.mName & _ ". Temperatura actuală este" & mHouse.HomeTemp) Altfel

Console.WriteLineC "Am in" & Me.mName & _ ". Temperatura actuală este" & mHouse.HouseTemp)

„Nu faceți nimic, temperatura este normală

End If Catch cravată ca ThreadlnterruptedException

„Așteptarea pasivă a fost întreruptă de Catch e As Exception

„Alte excepții

Sfârșit Încercați

Încheiați SyncLock

Sfârșitul Sub

Codul de bloc SyncLock este executat atomic. Accesul la acesta din toate celelalte fire va fi închis până când primul fir eliberează blocarea cu comanda End SyncLock. Dacă un fir dintr-un bloc sincronizat intră într-o stare de așteptare pasivă, blocarea rămâne până când firul este întrerupt sau reluat.

Utilizarea corectă a comenzii SyncLock vă menține firul de program în siguranță. Din păcate, utilizarea excesivă a SyncLock are un impact negativ asupra performanței. Sincronizarea codului într-un program multithread reduce viteza de lucru a acestuia de câteva ori. Sincronizați doar codul cel mai necesar și eliberați blocarea cât mai curând posibil.

Clasele de colecție de bază nu sunt sigure în aplicațiile cu mai multe fire, dar .NET Framework include versiuni sigure pentru majoritatea claselor de colecție. În aceste clase, codul metodelor potențial periculoase este inclus în blocurile SyncLock. Versiunile cu fire de siguranță ale claselor de colectare ar trebui utilizate în programe cu mai multe fire ori de câte ori este compromisă integritatea datelor.

Rămâne să menționăm că variabilele condiționale sunt ușor de implementat folosind comanda SyncLock. Pentru a face acest lucru, trebuie doar să sincronizați scrierea cu proprietatea booleană comună, disponibilă pentru citire și scriere, așa cum se face în următorul fragment:

Stare publică clasăVariabilă

Vestiar partajat privat ca obiect = obiect nou ()

Partajat privat mOK Ca partajat boolean

Proprietate TheConditionVariable () Ca boolean

obține

Întoarceți mOK

End Get

Setați (ByVal Value As Boolean) SyncLock (dulap)

mOK = Valoare

Încheiați SyncLock

Set final

Proprietate finală

Clasa de sfârșit

Clasa de comandă și monitorizare SyncLock

Utilizarea comenzii SyncLock implică câteva subtilități care nu au fost prezentate în exemplele simple de mai sus. Deci, alegerea obiectului de sincronizare joacă un rol foarte important. Încercați să rulați programul anterior cu comanda SyncLock (Me) în loc de SyncLock (mHouse). Temperatura crește din nou peste prag!

Amintiți-vă că comanda SyncLock se sincronizează folosind obiect, transmis ca parametru, nu de fragmentul de cod. Parametrul SyncLock acționează ca o ușă pentru accesarea fragmentului sincronizat din alte fire. Comanda SyncLock (Me) deschide de fapt mai multe „uși” diferite, exact ceea ce încercați să evitați cu sincronizarea. Moralitate:

Pentru a proteja datele partajate într-o aplicație cu mai multe fire, comanda SyncLock trebuie să sincronizeze câte un obiect.

Deoarece sincronizarea este asociată cu un anumit obiect, în unele situații, este posibil să se blocheze din greșeală alte fragmente. Să presupunem că aveți două metode sincronizate, prima și a doua, iar ambele metode sunt sincronizate pe obiectul bigLock. Când firul 1 intră mai întâi în metodă și capturează bigLock, niciun fir nu va putea introduce metoda în al doilea rând, deoarece accesul la acesta este deja limitat la firul 1!

Funcționalitatea comenzii SyncLock poate fi gândită ca un subset al funcționalității clasei Monitor. Clasa Monitor este extrem de personalizabilă și poate fi utilizată pentru a rezolva sarcini de sincronizare non-banale. Comanda SyncLock este un analog aproximativ al metodelor Enter și Exit din clasa Monitor:

Încerca

Monitor.Enter (theObject) În cele din urmă

Monitor.Exit (theObject)

Sfârșit Încercați

Pentru unele operații standard (creșterea / micșorarea unei variabile, schimbarea conținutului a două variabile), .NET Framework oferă clasa Interlocked, ale cărei metode efectuează aceste operații la nivel atomic. Folosind clasa Interlocked, aceste operații sunt mult mai rapide decât folosind comanda SyncLock.

Interconectare

În timpul sincronizării, blocarea este setată pe obiecte, nu pe fire, deci atunci când se utilizează diferit obiecte de blocat diferit fragmente de cod din programe apar uneori erori destul de non-banale. Din păcate, în multe cazuri sincronizarea pe un singur obiect este pur și simplu inacceptabilă, deoarece va duce la blocarea firelor prea des.

Luați în considerare situația interblocare(impas) în forma sa cea mai simplă. Imaginați-vă doi programatori la masa de cină. Din păcate, au doar un cuțit și o furculiță pentru doi. Presupunând că aveți nevoie atât de un cuțit, cât și de o furculiță pentru a mânca, sunt posibile două situații:

  • Un programator reușește să apuce un cuțit și o furculiță și începe să mănânce. Când este plin, lasă cina pusă deoparte și apoi un alt programator le poate lua.
  • Un programator ia cuțitul, iar celălalt ia furculița. Nici unul dintre ei nu poate începe să mănânce decât dacă celălalt renunță la aparatul său.

Într-un program multithread, această situație este numită blocare reciprocă. Cele două metode sunt sincronizate pe diferite obiecte. Firul A captează obiectul 1 și intră în porțiunea de program protejată de acest obiect. Din păcate, pentru a funcționa, are nevoie de acces la cod protejat de un alt bloc de sincronizare cu un alt obiect de sincronizare. Dar, înainte de a avea timp să introducă un fragment care este sincronizat de un alt obiect, fluxul B îl intră și captează acest obiect. Acum firul A nu poate introduce al doilea fragment, firul B nu poate intra în primul fragment și ambele fire sunt condamnate să aștepte la nesfârșit. Niciun fir nu poate continua să ruleze, deoarece obiectul cerut nu va fi niciodată eliberat.

Diagnosticul blocajelor este complicat de faptul că acestea pot apărea în cazuri relativ rare. Totul depinde de ordinea în care programatorul le alocă timp CPU. Este posibil ca, în majoritatea cazurilor, obiectele de sincronizare să fie capturate într-o ordine fără impas.

Următoarea este o implementare a situației de impas tocmai descrisă. După o scurtă discuție cu privire la cele mai fundamentale puncte, vom arăta cum să identificăm o situație de blocare în fereastra thread:

1 Opțiune Strict On

2 Sistem de import. Filetare

3 Modul Modul

4 Sub Main ()

5 Dim Tom ca nou programator („Tom”)

6 Dim Bob ca nou programator („Bob”)

7 Dim aThreadStart As New ThreadStart (AddressOf Tom.Eat)

8 Dim aThread Ca fir nou (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread ca fir nou (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start ()

14 bThread.Start ()

15 Sfârșitul Sub

16 Modul final

17 Furcă de clasă publică

18 Private Shared mForkAvaiTable As Boolean = True

19 Private Shared mOwner As String = "Nimeni"

20 Proprietatea de citire privată este proprietatea Utensil () ca șir

21 Ia

22 Întoarceți proprietarul

23 Sfârșitul primește

24 Proprietate finală

25 Sub public GrabForktByVal ca programator)

26 Console.Writel_ine (Thread.CurrentThread.Name & _

„încercând să apuc furculița”.)

27 Console.WriteLine (Me.OwnsUtensil & "are furculița."). ...

28 Monitor.Enter (Me) "SyncLock (aFork)"

29 Dacă mForkAvailable Atunci

30 a.HasFork = Adevărat

31 mProprietar = a.MyName

32 mForkAvailable = Fals

33 Console.WriteLine (a.MameName & "tocmai am primit furculița. Așteaptă")

34 Încearcă

Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)

Sfârșit Încercați

35 Sfârșit Dacă

36 Monitor.Exit (Me)

Încheiați SyncLock

37 Sfârșitul Sub

38 Clasa de sfârșit

39 Cuțit de clasă publică

40 Private Shared mKnifeAvailable As Boolean = True

41 Private Shared mOwner As String = "Nimeni"

42 Proprietatea de citire privată este proprietatea Utensi1 () ca șir

43 Ia

44 Întoarceți proprietarul

45 Sfârșit Obține

46 Proprietate finală

47 Sub public GrabKnifetByVal ca programator)

48 Console.WriteLine (Thread.CurrentThread.Name & _

„încercând să apuc cuțitul.”)

49 Console.WriteLine (Me.OwnsUtensil & "are cuțitul.")

50 Monitor.Enter (Me) "SyncLock (aKnife)"

51 Dacă mKnifeAvailable Atunci

52 mKnifeAvailable = False

53 a.HasKnife = Adevărat

54 mProprietar = a.MameName

55 Console.WriteLine (a.MameName & „tocmai am primit cuțitul. Așteaptă”)

56 Încearcă

Thread.Sleep (100)

Prindeți ca excepție

Console.WriteLine (e.StackTrace)

Sfârșit Încercați

57 Sfârșit Dacă

58 Monitor.Exit (Me)

59 Sfârșitul Sub

60 Clasa de sfârșit

61 Programator public

62 Private mName As String

63 Private Shared mFork As Fork

64 mKnife Private Shared As Knife

65 Private mHasKnife Ca boolean

66 Private mHasFork As Boolean

67 Partajat Sub Nou ()

68 mFork = New Fork ()

69 m Cuțit = Cuțit nou ()

70 Sfârșitul Sub

71 Sub public Nou (ByVal theName As String)

72 mName = theName

73 Sfârșitul Sub

74 Proprietatea de citire publică MyName () Ca șir

75 Ia

76 Returnează mName

77 Sfârșitul primește

78 Proprietate finală

79 Proprietate publică HasKnife () Ca boolean

80 Ia

81 Returnează mHasKnife

82 Sfârșitul primește

83 Set (ByVal Value As Boolean)

84 mHasKnife = Valoare

85 Set final

86 Proprietate finală

87 Proprietate publică HasFork () Ca boolean

88 Ia

89 Returnează mHasFork

90 Sfârșit Obține

91 Set (ByVal Value As Boolean)

92 mHasFork = Valoare

93 Set final

94 Proprietate finală

95 Public Sub Eat ()

96 Fă până la mine.HasKnife and Me.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "este în thread.")

98 Dacă Rnd ()< 0.5 Then

99 mFork.GrabFork (Me)

100 Altfel

101 mKnife.GrabKnife (Eu)

102 Sfârșit Dacă

103 Bucla

104 MsgBox (Me.MyName & "pot mânca!")

105 m Cuțit = Cuțit nou ()

106 mFork = New Fork ()

107 Sfârșitul Sub

108 Clasa de sfârșit

Procedura principală Main (liniile 4-16) creează două instanțe ale clasei Programmer și apoi pornește două fire pentru a executa metoda critică Eat a clasei Programmer (liniile 95-108), descrisă mai jos. Procedura principală stabilește numele firelor și le configurează; probabil tot ce se întâmplă este de înțeles și fără comentarii.

Codul pentru clasa Fork arată mai interesant (liniile 17-38) (o clasă similară a cuțitelor este definită în liniile 39-60). Rândurile 18 și 19 specifică valorile câmpurilor comune, prin care puteți afla dacă fișa este disponibilă în prezent și, dacă nu, cine o folosește. Proprietatea ReadOnly OwnUtensi1 (rândurile 20-24) este destinată celui mai simplu transfer de informații. Elementul central al clasei Fork este metoda GrabFork „apuca furculița”, definită în liniile 25-27.

  1. Liniile 26 și 27 pur și simplu imprimă informații de depanare pe consolă. În codul principal al metodei (liniile 28-36), accesul la furcă este sincronizat de obiectcentură-mă. Deoarece programul nostru folosește doar o singură furcă, sincronizarea Me asigură că nu există două fire care să o poată lua în același timp. Comanda Slee "p (în blocul care începe pe linia 34) simulează întârzierea între apucarea unei furculițe / cuțit și începutul mesei. Rețineți că comanda Sleep nu deblochează obiecte și accelerează doar blocările!
    Cu toate acestea, cel mai interesant este codul clasei Programator (liniile 61-108). Liniile 67-70 definesc un constructor generic pentru a se asigura că există doar o singură furculiță și cuțit în program. Codul proprietății (rândurile 74-94) este simplu și nu necesită comentarii. Cel mai important lucru se întâmplă în metoda Eat, care este executată de două fire separate. Procesul continuă într-o buclă până când un flux captează furculița împreună cu cuțitul. Pe liniile 98-102, obiectul apucă în mod aleatoriu furculița / cuțitul folosind apelul Rnd, ceea ce cauzează blocajul. Se întâmplă următoarele:
    Firul care execută metoda Eat a obiectului Thoth este invocat și intră în buclă. Apucă cuțitul și intră într-o stare de așteptare.
  2. Firul care execută metoda Bob's Eat este invocat și intră în buclă. Nu poate apuca cuțitul, dar apucă furculița și intră într-o stare de așteptare.
  3. Firul care execută metoda Eat a obiectului Thoth este invocat și intră în buclă. Încearcă să apuce furculița, dar Bob a apucat deja furca; firul intră într-o stare de așteptare.
  4. Firul care execută metoda Bob's Eat este invocat și intră în buclă. Încearcă să apuce cuțitul, dar cuțitul este deja capturat de obiectul Thoth; firul intră într-o stare de așteptare.

Toate acestea continuă la nesfârșit - ne confruntăm cu o situație tipică de impas (încercați să rulați programul și veți vedea că nimeni nu poate mânca așa).
De asemenea, puteți verifica dacă a apărut un blocaj în fereastra de fire. Rulați programul și întrerupeți-l cu tastele Ctrl + Break. Includeți variabila Me în fereastra de vizualizare și deschideți fereastra de fluxuri. Rezultatul arată ceva asemănător celui prezentat în Fig. 10.7. Din figură, puteți vedea că firul lui Bob a apucat un cuțit, dar nu are furculiță. Faceți clic dreapta în fereastra Threads de pe linia Tot și selectați comanda Switch to Thread din meniul contextual. Graficul arată că pârâul Thoth are o furculiță, dar nu are cuțit. Desigur, aceasta nu este o dovadă sută la sută, dar un astfel de comportament mă face cel puțin să bănuiți că ceva nu era în regulă.
Dacă opțiunea cu sincronizarea cu un singur obiect (ca în programul cu creșterea temperaturii -în casă) nu este posibilă, pentru a preveni blocarea reciprocă, puteți numera obiectele de sincronizare și le puteți captura întotdeauna într-o ordine constantă. Să continuăm analogia programatorului de mese: dacă firul ia întotdeauna cuțitul mai întâi și apoi furculița, nu vor exista probleme cu blocarea. Primul flux care apucă cuțitul va putea mânca normal. Tradus în limbajul fluxurilor de programe, acest lucru înseamnă că captarea obiectului 2 este posibilă numai dacă obiectul 1 este capturat mai întâi.

Orez. 10.7. Analiza blocajelor în fereastra firului

Prin urmare, dacă eliminăm apelul către Rnd pe linia 98 și îl înlocuim cu fragmentul

mFork.GrabFork (Eu)

mKnife.GrabKnife (Eu)

impasul dispare!

Colaborați la date pe măsură ce sunt create

În aplicațiile cu mai multe fire, există adesea o situație în care firele nu funcționează numai cu date partajate, ci și așteaptă să apară (adică firul 1 trebuie să creeze date înainte ca firul 2 să îl poată utiliza). Deoarece datele sunt partajate, accesul la acestea trebuie sincronizat. De asemenea, este necesar să se furnizeze mijloace pentru notificarea firelor de așteptare cu privire la apariția datelor gata.

Această situație se numește de obicei problema furnizorului / consumatorului. Firul încearcă să acceseze date care nu există încă, deci trebuie să transfere controlul către un alt fir care creează datele necesare. Problema este rezolvată cu următorul cod:

  • Firul 1 (consumator) se trezește, introduce o metodă sincronizată, caută date, nu le găsește și intră într-o stare de așteptare. Preliminardin punct de vedere fizic, el trebuie să înlăture blocajul pentru a nu interfera cu munca firului de alimentare.
  • Firul 2 (furnizor) introduce o metodă sincronizată eliberată de firul 1, creează date pentru fluxul 1 și notifică cumva fluxul 1 despre prezența datelor. Apoi eliberează blocarea, astfel încât firul 1 să poată procesa noile date.

Nu încercați să rezolvați această problemă invocând în mod constant firul 1 și verificând starea variabilei de condiție, a cărei valoare este> setată de firul 2. Această decizie va afecta grav performanța programului dvs., deoarece în majoritatea cazurilor firul 1 va afecta să fie invocat fără niciun motiv; iar firul 2 va aștepta atât de des încât va rămâne fără timp pentru a crea date.

Relațiile furnizor / consumator sunt foarte frecvente, astfel încât sunt create primitive speciale pentru astfel de situații în bibliotecile de clase de programare multithread. În NET, aceste primitive se numesc Wait și Pulse-PulseAl 1 și fac parte din clasa Monitor. Figura 10.8 ilustrează situația pe care urmează să o programăm. Programul organizează trei cozi thread: o coadă de așteptare, o coadă de blocare și o coadă de execuție. Programatorul de fire nu alocă timpul procesorului firelor care se află în coada de așteptare. Pentru ca un fir să fie alocat timp, acesta trebuie să treacă la coada de execuție. Ca rezultat, activitatea aplicației este organizată mult mai eficient decât cu sondarea obișnuită a unei variabile condiționale.

În pseudocod, expresia consumatorului de date este formulată după cum urmează:

"Intrarea într - un bloc sincronizat de tipul următor

În timp ce nu există date

Mergeți la coada de așteptare

Buclă

Dacă există date, procesați-le.

Părăsiți blocul sincronizat

Imediat după executarea comenzii Așteptați, firul este suspendat, blocarea este eliberată și firul intră în coada de așteptare. Când blocarea este eliberată, firul din coada de execuție este permis să ruleze. În timp, unul sau mai multe fire blocate vor crea datele necesare pentru funcționarea firului care se află în coada de așteptare. Deoarece validarea datelor se efectuează într-o buclă, trecerea la utilizarea datelor (după buclă) are loc numai atunci când există date pregătite pentru procesare.

În pseudocod, expresia furnizorului de date arată astfel:

"Introducerea unui bloc de vizualizare sincronizat

În timp ce datele NU sunt necesare

Mergeți la coada de așteptare

Altfel Produce date

Când datele sunt gata, apelați Pulse-PulseAll.

pentru a muta unul sau mai multe fire din coada de blocare în coada de execuție. Părăsiți blocul sincronizat (și reveniți la coada de rulare)

Să presupunem că programul nostru simulează o familie cu un părinte care face bani și un copil care cheltuie acești bani. Când banii s-au terminatse pare că copilul trebuie să aștepte sosirea unei noi sume. Implementarea software-ului acestui model arată astfel:

1 Opțiune Strict On

2 Sistem de import. Filetare

3 Modul Modul

4 Sub Main ()

5 Dim theFamily As New Family ()

6 theFamily.StartltsLife ()

7 Sfârșitul Sub

8 Sfârșitul fjodulei

9

10 familii de clasă publică

11 Private mMoney As Integer

12 Private mWeek As Integer = 1

13 Public Sub StartltsLife ()

14 Dim aThread Începeți ca New ThreadStarUAddressOf Me.Produce)

15 Dim bThread Începeți ca nou ThreadStarUAddressOf Me. Consumați)

16 Dim aThread Ca fir nou (aThreadStart)

17 Dim bThread ca fir nou (bThreadStart)

18 aThread.Name = "Produce"

19 aThread.Start ()

20 bThread.Name = "Consumă"

21 bFilet. Start ()

22 Sfârșitul Sub

23 Proprietate publică TheWeek () Ca întreg

24 Ia

25 Întoarceți-vă săptămâna

26 Sfârșit Obține

27 Set (valoare ByVal ca număr întreg)

28 săptămână - Valoare

29 Set final

30 Proprietate finală

31 Proprietate publică OurMoney () Ca întreg

32 Ia

33 Returnează mMoney

34 Sfârșit Obține

35 Set (ByVal Value As Inger)

36 mBani = Valoare

37 Set final

38 Proprietate finală

39 Subproduse publice ()

40 Thread.Sleep (500)

41 Fă

42 Monitor.Inter (Me)

43 Fă în timp ce eu. Banii noștri> 0

44 Monitor.Așteptați (Eu)

45 Buclă

46 Me.OurMoney = 1000

47 Monitor.PulseAll (Me)

48 Monitor.Exit (Me)

49 Buclă

50 Sfârșitul Sub

51 Subconsum public ()

52 MsgBox („Sunt în fir de consum”)

53 Fă

54 Monitor.Inter (Me)

55 Fă în timp ce eu. Banii noștri = 0

56 Monitor.Așteptați (Eu)

57 Buclă

58 Console.WriteLine („Dragă părinte, tocmai ți-am petrecut toate” & _

bani în săptămână "și TheWeek)

59 Săptămâna + = 1

60 Dacă TheWeek = 21 * 52 atunci System.Environment.Exit (0)

61 Me.OurMoney = 0

62 Monitor.PulseAll (Me)

63 Monitor.Exit (Me)

64 Buclă

65 Sfârșitul Sub

66 Clasa de sfârșit

Metoda StartltsLife (liniile 13-22) se pregătește să înceapă fluxurile Produce și Consum. Cel mai important lucru se întâmplă în fluxurile Produce (liniile 39-50) și Consum (liniile 51-65). Procedura Subproduce verifică disponibilitatea banilor și, dacă există bani, se duce la coada de așteptare. În caz contrar, părintele generează bani (linia 46) și notifică obiectele din coada de așteptare despre o modificare a situației. Rețineți că apelul către Pulse-Pulse All are efect numai atunci când blocarea este eliberată cu comanda Monitor.Exit. Dimpotrivă, procedura Sub Consum verifică disponibilitatea banilor și, dacă nu există bani, notifică părintele care se așteaptă. Linia 60 încetează pur și simplu programul după 21 de ani condiționati; Sistem de apelare. Environment.Exit (0) este analogul .NET al comenzii End (este acceptată și comanda End, dar spre deosebire de System. Environment. Exit, nu returnează un cod de ieșire la sistemul de operare).

Subiectele care sunt puse în coada de așteptare trebuie eliberate de alte părți ale programului. Din acest motiv, preferăm să folosim PulseAll peste Pulse. Deoarece nu se știe în prealabil ce fir de activare va fi activat când se apelează Pulse 1, cu un număr relativ mic de fire de execuție în coadă, puteți apela la fel de bine PulseAll.

Multithreading în programe grafice

Discuția noastră despre multithreading în aplicații GUI începe cu un exemplu care explică la ce servește multithreading în aplicații GUI. Creați un formular cu două butoane Start (btnStart) și Cancel (btnCancel), așa cum se arată în Fig. 10.9. Dând clic pe butonul Start se generează o clasă care conține un șir aleatoriu de 10 milioane de caractere și o metodă de numărare a aparițiilor literei „E” în ​​acel șir lung. Rețineți utilizarea clasei StringBuilder pentru crearea mai eficientă a șirurilor lungi.

Pasul 1

Firul 1 observă că nu există date pentru acesta. Apelează Wait, eliberează blocarea și merge la coada de așteptare.



Pasul 2

Când blocarea este eliberată, firul 2 sau firul 3 părăsesc coada blocului și intră într-un bloc sincronizat, dobândind blocarea

Pasul 3

Să presupunem că firul 3 intră într-un bloc sincronizat, creează date și apelează Pulse-Pulse All.

Imediat după ieșirea din bloc și eliberarea blocării, firul 1 este mutat în coada de execuție. Dacă firul 3 apelează Pluse, doar unul intră în coada de execuțiethread, când Pluse All este apelat, toate thread-urile merg la coada de execuție.



Orez. 10.8. Problema furnizorului / consumatorului

Orez. 10.9. Multithreading într-o aplicație simplă GUI

Imports System.Text

Public Class RandomCharacters

Private m_Data Ca StringBuilder

Private mjength, m_count Ca întreg

Public Sub New (ByVal n As Integer)

m_Length = n -1

m_Data = New StringBuilder (m_length) MakeString ()

Sfârșitul Sub

Private Sub MakeString ()

Dim i Integer

Dim myRnd As New Random ()

Pentru i = 0 La m_lungime

„Generați un număr aleatoriu între 65 și 90,

"convertiți - l în majuscule

"și atașați la obiectul StringBuilder

m_Data.Append (Chr (myRnd.Next (65.90)))

Următorul

Sfârșitul Sub

Public Sub StartCount ()

GetEes ()

Sfârșitul Sub

Sub GetEes privat ()

Dim i Integer

Pentru i = 0 La m_lungime

Dacă m_Data.Chars (i) = CChar ("E") Atunci

m_count + = 1

Încheiați dacă Următorul

m_CountDone = Adevărat

Sfârșitul Sub

Citire publică numai

Proprietate GetCount () După cum obține Integer

Dacă nu (m_CountDone) Atunci

Returnează m_count

End If

End Get End Property

Citire publică numai

Proprietatea IsDone () ca Boolean Get

Întoarcere

m_CountDone

End Get

Proprietate finală

Clasa de sfârșit

Există un cod foarte simplu asociat celor două butoane din formular. Procedura btn-Start_Click instanțiază clasa RandomCharacters de mai sus, care încapsulează un șir cu 10 milioane de caractere:

Private Sub btnStart_Click (expeditor ByVal ca System.Object.

ByVal e As System.EventArgs) Manevrează btnSTart.Click

Dim RC Ca caractere aleatoare noi (10000000)

RC.StartCount ()

MsgBox („Numărul de e este” & RC.GetCount)

Sfârșitul Sub

Butonul Anulare afișează o casetă de mesaj:

Sub privat btnCancel_Click (expeditor ByVal ca System.Object._

ByVal e As System.EventArgs) Manevrează btnCancel.Click

MsgBox („Numărul întrerupt!”)

Sfârșitul Sub

Când programul este rulat și butonul Start este apăsat, se dovedește că butonul Cancel nu răspunde la introducerea utilizatorului, deoarece bucla continuă împiedică butonul să gestioneze evenimentul pe care îl primește. Acest lucru este inacceptabil în programele moderne!

Există două soluții posibile. Prima opțiune, bine cunoscută din versiunile anterioare VB, renunță la multithreading: apelul DoEvents este inclus în buclă. În NET această comandă arată astfel:

Application.DoEvents ()

În exemplul nostru, acest lucru nu este cu siguranță de dorit - cine vrea să încetinească un program cu zece milioane de apeluri DoEvents! Dacă în schimb alocați bucla unui fir separat, sistemul de operare va comuta între fire și butonul Anulare va rămâne funcțional. Implementarea cu un fir separat este prezentată mai jos. Pentru a arăta clar că butonul Anulare funcționează, atunci când facem clic pe el, terminăm pur și simplu programul.

Pasul următor: butonul Afișare numărare

Să presupunem că ați decis să vă arătați imaginația creativă și să dați formei aspectul arătat în fig. 10.9. Rețineți: butonul Afișare numărare nu este încă disponibil.

Orez. 10.10. Forma butonului blocat

Se așteaptă ca un fir separat să facă numărarea și să deblocheze butonul indisponibil. Desigur, acest lucru se poate face; în plus, o astfel de sarcină apare destul de des. Din păcate, nu veți putea acționa în modul cel mai evident - conectați firul secundar la firul GUI păstrând o legătură către butonul ShowCount din constructor sau chiar folosind un delegat standard. Cu alte cuvinte, nu nu utilizați opțiunea de mai jos (de bază eronat liniile sunt cu caractere aldine).

Public Class RandomCharacters

Private m_0ata Ca StringBuilder

Private m_CountDone Ca boolean

Lungime privată. m_count Ca întreg

Private m_Button Ca Windows.Forms.Button

Public Sub New (ByVa1 n Ca întreg, _

ByVal b Ca Windows.Forms.Button)

m_length = n - 1

m_Data = StringBuilder nou (mJength)

m_Button = b MakeString ()

Sfârșitul Sub

Private Sub MakeString ()

Dim I Ca întreg

Dim myRnd As New Random ()

Pentru I = 0 La m_lungime

m_Data.Append (Chr (myRnd.Next (65.90)))

Următorul

Sfârșitul Sub

Public Sub StartCount ()

GetEes ()

Sfârșitul Sub

Sub GetEes privat ()

Dim I Ca întreg

Pentru I = 0 La lungime

Dacă m_Data.Chars (I) = CChar ("E") Atunci

m_count + = 1

Încheiați dacă Următorul

m_CountDone = Adevărat

m_Button.Enabled = Adevărat

Sfârșitul Sub

Citire publică numai

Proprietate GetCount () Ca întreg

obține

Dacă nu (m_CountDone) Atunci

Aruncați o excepție nouă („Numărul nu a fost încă făcut”) Altfel

Returnează m_count

End If

End Get

Proprietate finală

Proprietate publică numai în citire IsDone () Ca boolean

obține

Returnează m_CountDone

End Get

Proprietate finală

Clasa de sfârșit

Este probabil ca acest cod să funcționeze în unele cazuri. Cu toate acestea:

  • Interacțiunea firului secundar cu firul care creează interfața grafică nu poate fi organizată evident mijloace.
  • Nu nu modificați elementele din programele grafice din alte fluxuri de programe. Toate modificările trebuie să apară numai în firul care a creat GUI.

Dacă încălcați aceste reguli, noi garantăm că bug-urile subtile și subtile vor apărea în programele dvs. grafice cu mai multe fire.

De asemenea, nu va reuși să organizeze interacțiunea obiectelor folosind evenimente. Lucrătorul din 06 evenimente rulează pe același fir pe care a fost numit RaiseEvent, astfel încât evenimentele nu vă vor ajuta.

Totuși, bunul simț dictează faptul că aplicațiile grafice trebuie să aibă un mijloc de a modifica elemente dintr-un alt fir. În NET Framework, există un mod sigur de a apela la metode de aplicații GUI dintr-un alt fir. În acest scop se utilizează un tip special de delegat Method Invoker din spațiul de nume System.Windows. Formulare. Următorul fragment prezintă o nouă versiune a metodei GetEes (liniile modificate cu caractere aldine):

Sub GetEes privat ()

Dim I Ca întreg

Pentru I = 0 La m_lungime

Dacă m_Data.Chars (I) = CChar ("E") Atunci

m_count + = 1

Încheiați dacă Următorul

m_CountDone = Încearcă adevărat

Dim mylnvoker ca metodă nouălnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

„Eșec

Sfârșit Încercați

Sfârșitul Sub

Public Sub UpDateButton ()

m_Button.Enabled = Adevărat

Sfârșitul Sub

Apelurile inter-thread către buton se fac nu direct, ci prin Method Invoker. .NET Framework garantează că această opțiune este sigură pentru fire.

De ce sunt atât de multe probleme cu programarea multithread?

Acum, că aveți o oarecare înțelegere a multithreading-ului și a potențialelor probleme asociate acestuia, am decis că ar fi potrivit să răspundem la întrebarea din titlul acestei subsecțiuni la sfârșitul acestui capitol.

Unul dintre motive este că multithreading-ul este un proces neliniar și suntem obișnuiți cu un model de programare liniară. La început, este dificil să te obișnuiești cu ideea că execuția programului poate fi întreruptă aleatoriu, iar controlul va fi transferat în alt cod.

Cu toate acestea, există un alt motiv, mai fundamental: în zilele noastre programatorii programează prea rar în asamblare sau cel puțin se uită la ieșirea dezasamblată a compilatorului. În caz contrar, ar fi mult mai ușor pentru ei să se obișnuiască cu ideea că zeci de instrucțiuni de asamblare pot corespunde unei singure comenzi a unui limbaj de nivel înalt (cum ar fi VB .NET). Firul poate fi întrerupt după oricare dintre aceste instrucțiuni și, prin urmare, în mijlocul unei comenzi de nivel înalt.

Dar asta nu este tot: compilatoarele moderne optimizează performanța programului, iar hardware-ul computerului poate interfera cu gestionarea memoriei. Ca rezultat, compilatorul sau hardware-ul poate modifica ordinea comenzilor specificate în codul sursă al programului fără știrea dumneavoastră [ Mulți compilatori optimizează copierea ciclică a matricelor, cum ar fi pentru i = 0 la n: b (i) = a (i): ncxt. Compilatorul (sau chiar un manager de memorie specializat) poate crea pur și simplu un tablou și apoi îl poate completa cu o singură operație de copiere în loc să copieze elemente individuale de multe ori!].

Sperăm că aceste explicații vă vor ajuta să înțelegeți mai bine de ce programarea multithread cauzează atâtea probleme - sau cel puțin mai puțin surprinsă de comportamentul ciudat al programelor dvs. multi-thread!

Un exemplu de construire a unei aplicații simple cu mai multe fire.

Născut din cauza multor întrebări despre construirea aplicațiilor multithread în Delphi.

Scopul acestui exemplu este de a demonstra cum să construiți corect o aplicație cu mai multe fire, cu eliminarea lucrărilor pe termen lung într-un fir separat. Și cum, într-o astfel de aplicație, să asigurați interacțiunea firului principal cu lucrătorul pentru transferul de date din formular (componente vizuale) în flux și invers.

Exemplul nu pretinde a fi complet, doar demonstrează cele mai simple modalități de interacțiune între fire. Permițând utilizatorului să „orbească rapid” (cine ar ști cât de mult îl urăsc) o aplicație multithreaded care funcționează corect.
Totul din el este comentat în detaliu (după părerea mea), dar dacă aveți întrebări, întrebați.
Dar încă o dată te avertizez: Fluxurile nu sunt ușoare... Dacă nu aveți nicio idee despre cum funcționează totul, atunci există un pericol imens că de multe ori totul va funcționa bine pentru dvs. și, uneori, programul se va comporta mai mult decât ciudat. Comportamentul unui program multithread scris incorect depinde în mare măsură de un număr mare de factori care uneori nu pot fi reproduse în timpul depanării.

Deci un exemplu. Pentru comoditate, am plasat atât codul, cât și am atașat arhiva cu modulul și codul formularului

unitate ExThreadForm;

utilizări
Windows, mesaje, SysUtils, variante, clase, grafică, controale, formulare,
Dialoguri, StdCtrls;

// constante utilizate la transferul datelor dintr-un flux într-un formular folosind
// trimite mesaje în fereastră
const
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

tip
// descrierea clasei firului, un descendent al tThread
tMyThread = clasă (tThread)
privat
SyncDataN: întreg;
SyncDataS: String;
procedura SyncMetod1;
protejat
procedura Execute; trece peste;
public
Param1: Șir;
Param2: Număr întreg;
Param3: Boolean;
Oprit: Boolean;
LastRandom: Integer;
Nr. De iterație: întreg;
ResultList: tStringList;

Constructor Create (aParam1: String);
distructor Distruge; trece peste;
Sfârșit;

// descrierea clasei formularului folosind fluxul
TForm1 = clasă (TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btnStartClick (Sender: TObject);
procedure btnStopClick (Sender: TObject);
privat
(Declarații private)
MyThread: tMyThread;
procedure EventMyThreadOnTerminate (Expeditor: tObject);
procedure EventOnSendMessageMetod (var Msg: TMessage); mesaj WM_USER_SendMessageMetod;
procedure EventOnPostMessageMetod (var Msg: TMessage); mesaj WM_USER_PostMessageMetod;

Public
(Declarații publice)
Sfârșit;

var
Form1: TForm1;

{
Oprit - Demonstră transferul de date dintr-un formular într-un flux.
Nu este necesară sincronizarea suplimentară, deoarece este simplă
cu un singur cuvânt și este scris de un singur fir.
}

procedura TForm1.btnStartClick (Expeditor: TObject);
începe
Randomize (); // asigurarea aleatoriei în secvență de către Random () - nu are nimic de-a face cu fluxul

// Creați o instanță a obiectului de flux, trecându-i un parametru de intrare
{
ATENŢIE!
Constructorul fluxului este scris în așa fel încât fluxul să fie creat
suspendat deoarece permite:
1. Controlează momentul lansării sale. Acest lucru este aproape întotdeauna mai convenabil pentru că
vă permite să configurați un flux chiar înainte de a începe, treceți-l de intrare
parametrii etc.
2. Pentru că linkul către obiectul creat va fi salvat în câmpul formularului, apoi
după autodistrugerea firului (vezi mai jos) care atunci când firul rulează
poate apărea în orice moment, acest link va deveni invalid.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Cu toate acestea, din moment ce firul a fost creat suspendat, atunci cu privire la orice erori
// în timpul inițializării sale (înainte de a începe), trebuie să-l distrugem noi înșine
// pentru ceea ce folosim try / except block
încerca

// Atribuirea unui handler de terminare a firului în care vom primi
// rezultatele activității fluxului și „suprascrieți” linkul către acesta
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Deoarece rezultatele vor fi colectate în OnTerminate, adică înainte de autodistrugere
// fluxul atunci vom scoate grijile distrugerii lui
MyThread.FreeOnTerminate: = Adevărat;

// Un exemplu de trecere a parametrilor de intrare prin câmpurile obiectului flux, la punctul respectiv
// instantanee când nu rulează deja.
// Personal, prefer să fac asta prin parametrii suprascrisului
// constructor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // și un fel de parametru, dar care se schimbă
// timpul de rulare a firului
cu exceptia
// întrucât firul nu a început încă și nu se va putea autodistruge, îl vom distruge „manual”
FreeAndNil (MyThread);
// și apoi lăsați excepția să fie tratată ca de obicei
a ridica;
Sfârșit;

// Deoarece obiectul thread a fost creat și configurat cu succes, este timpul să îl porniți
MyThread.Resume;

ShowMessage („Fluxul a început”);
Sfârșit;

procedura TForm1.btnStopClick (Expeditor: TObject);
începe
// Dacă instanța firului încă există, atunci cereți-i să se oprească
// Și, exact „cere”. În principiu, putem și „forța”, dar o va face
// opțiune extrem de urgentă, care necesită o înțelegere clară a tuturor acestor lucruri
// bucătărie în flux. Prin urmare, nu este luat în considerare aici.
dacă este atribuit (MyThread) atunci
MyThread.Stopped: = Adevărat
altceva
ShowMessage ("Firul nu rulează!");
Sfârșit;

procedura TForm1.EventOnSendMessageMetod (var Msg: TMessage);
începe
// metoda de procesare a unui mesaj sincron
// în WParam adresa obiectului tMyThread, în LParam valoarea curentă a LastRandom a firului
cu tMyThread (Msg.WParam) începe
Form1.Label3.Caption: = Format ("% d% d% d",);
Sfârșit;
Sfârșit;

procedura TForm1.EventOnPostMessageMetod (var Msg: TMessage);
începe
// metoda de tratare a unui mesaj asincron
// în WParam valoarea curentă a IterationNo, în LParam valoarea curentă a fluxului LastRandom
Form1.Label4.Caption: = Format ("% d% d",);
Sfârșit;

procedura TForm1.EventMyThreadOnTerminate (Expeditor: tObject);
începe
// IMPORTANT!
// Metoda de gestionare a evenimentului OnTerminate este întotdeauna apelată în contextul main
// thread - acest lucru este garantat de implementarea tThread. Prin urmare, în ea poți în mod liber
// utilizați orice proprietăți și metode ale oricăror obiecte

// În orice caz, asigurați-vă că instanța obiect încă există
dacă nu este Asignat (MyThread), apoi Exit; // dacă nu este acolo, atunci nu este nimic de făcut

// obțineți rezultatele muncii thread-ului instanței obiectului thread
Form1.Memo1.Lines.Add (Format („Fluxul s-a încheiat cu rezultatul% d”,));
Form1.Memo1.Lines.AddStrings ((Expeditor ca tMyThread) .ResultList);

// Distrugeți referința la instanța obiectului de flux.
// Deoarece firul nostru se autodistruge (FreeOnTerminate: = True)
// apoi după finalizarea handlerului OnTerminate, instanța obiectului de flux va fi
// distrus (gratuit) și toate referințele la acesta vor deveni invalide.
// Pentru a nu rula accidental într-un astfel de link, suprascrieți MyThread
// Voi nota încă o dată - nu vom distruge obiectul, ci doar vom suprascrie linkul. Un obiect
// se distruge singur!
MyThread: = Nil;
Sfârșit;

constructorul tMyThread.Create (aParam1: String);
începe
// Creați o instanță a fluxului SUSPENDAT (consultați comentariul când creați instanțe)
Creați moștenit (Adevărat);

// Creați obiecte interne (dacă este necesar)
ResultList: = tStringList.Create;

// Obțineți date inițiale.

// Copiați datele de intrare trecute prin parametru
Param1: = aParam1;

// Un exemplu de primire a datelor de intrare de la componentele VCL în constructorul unui obiect de flux
// Acest lucru este acceptabil în acest caz, deoarece constructorul este apelat în context
// fir principal. Prin urmare, componentele VCL pot fi accesate aici.
// Dar, nu-mi place asta, pentru că mi se pare rău când firul știe ceva
// despre o formă acolo. Dar, ce nu poți face pentru demonstrație.
Param3: = Form1.CheckBox1.Checked;
Sfârșit;

destructor tMyThread.Destroy;
începe
// distrugerea obiectelor interne
FreeAndNil (ResultList);
// distruge baza tThread
mostenit;
Sfârșit;

procedura tMyThread.Execute;
var
t: Cardinal;
s: String;
începe
Nr.Iteratie: = 0; // contor de rezultate (numărul ciclului)

// În exemplul meu, corpul firului este o buclă care se termină
// sau printr-o „cerere” externă de terminare trecută prin parametrul variabil Stop,
// fie doar făcând 5 bucle
// Este mai plăcut pentru mine să scriu asta printr-o buclă „eternă”.

În timp ce adevărul începe

Inc (IterationNo); // numărul ciclului următor

LastRandom: = Random (1000); // număr cheie - pentru a demonstra transferul parametrilor de la flux la formular

T: = Aleator (5) +1; // timp pentru care vom adormi dacă nu suntem finalizați

// Lucru prost (în funcție de parametrul de intrare)
dacă nu Param3 atunci
Inc (Param2)
altceva
Dec (Param2);

// Formați un rezultat intermediar
s: = Format ("% s% 5d% s% d% d",
);

// Adăugați un rezultat intermediar la lista de rezultate
ResultList.Add (s);

//// Exemple de transmitere a unui rezultat intermediar unei forme

//// Trecerea printr-o metodă sincronizată - modul clasic
//// Dezavantaje:
//// - metoda sincronizată este de obicei o metodă a clasei de flux (pentru a accesa
//// la câmpurile obiectului de flux), dar, pentru a accesa câmpurile de formular, trebuie
//// „știu” despre acesta și câmpurile (obiectele) sale, ceea ce de obicei nu este foarte bun cu
//// punctul de vedere al organizării programului.
//// - firul curent va fi suspendat până la finalizarea executării
//// metoda sincronizată.

//// Avantaje:
//// - standard și versatil
//// - într-o metodă sincronizată, puteți utiliza
//// toate câmpurile obiectului de flux.
// mai întâi, dacă este necesar, trebuie să salvați datele transferate în
// câmpuri speciale ale obiectului obiect.
SyncDataN: = IterationNo;
SyncDataS: = "Sincronizare" + s;
// și apoi furnizați un apel de metodă sincronizată
Sincronizare (SyncMetod1);

//// Trimiterea prin trimiterea sincronă a mesajelor (SendMessage)
//// în acest caz, datele pot fi transmise atât prin parametrii mesajului (LastRandom),
//// și prin câmpurile obiectului, trecând adresa instanței în parametrul mesajului
//// a obiectului stream - Număr întreg (Self).
//// Dezavantaje:
//// - firul trebuie să cunoască mânerul ferestrei formularului
//// - la fel ca la Sincronizare, firul curent va fi suspendat până la
//// finalizarea procesării mesajului de către firul principal
//// - necesită o cantitate semnificativă de timp CPU pentru fiecare apel
//// (pentru a schimba firele), prin urmare, apelul foarte frecvent nu este de dorit
//// Avantaje:
//// - la fel ca la Sincronizare, atunci când procesați un mesaj, îl puteți utiliza
//// toate câmpurile obiectului fluxului (dacă, desigur, adresa acestuia a fost trecută)


//// pornește firul.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Transfer prin trimiterea mesajelor asincrone (PostMessage)
//// Deoarece în acest caz, până când mesajul este primit de firul principal,
//// este posibil ca fluxul de trimitere să fi terminat deja, trecând adresa instanței
//// obiectul fluxului nu este valid!
//// Dezavantaje:
//// - firul trebuie să cunoască mânerul ferestrei formularului;
//// - din cauza asincroniei, transferul de date este posibil doar prin intermediul parametrilor
//// mesaje, care complică semnificativ transferul de date care are o dimensiune
//// mai mult de două cuvinte mașină. Este convenabil de utilizat pentru trecerea întregului etc.
//// Avantaje:
//// - spre deosebire de metodele anterioare, firul curent NU va fi
//// este întrerupt și va relua imediat execuția
//// - spre deosebire de un apel sincronizat, un gestionar de mesaje
//// este o metodă de formular care trebuie să cunoască obiectul fluxului,
//// sau nu știi deloc despre flux dacă datele sunt transmise numai
//// prin intermediul parametrilor mesajului. Adică este posibil ca firul să nu știe nimic despre formular.
//// în general - doar Handle-ul ei, care poate fi trecut ca parametru înainte
//// pornește firul.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Verificați dacă este posibilă finalizarea

// Verificați finalizarea în funcție de parametru
dacă oprit, atunci Break;

// Verificați finalizarea ocazional
if IterationNo> = 10 atunci Break;

Somn (t * 1000); // Adormiți t secunde
Sfârșit;
Sfârșit;

procedura tMyThread.SyncMetod1;
începe
// această metodă este apelată prin metoda Sincronizare.
// Adică, în ciuda faptului că este o metodă a firului tMyThread,
// rulează în contextul firului principal al aplicației.
// Prin urmare, poate face orice, bine sau aproape orice :)
// Dar amintiți-vă, nu merită să vă „deranjați” mult timp

// Parametrii trecuți, îi putem extrage din câmpurile speciale, unde le avem
// salvat înainte de a apela.
Form1.Label1.Caption: = SyncDataS;

// fie din alte câmpuri ale obiectului flux, de exemplu, reflectând starea sa curentă
Form1.Label2.Caption: = Format ("% d% d",);
Sfârșit;

În general, exemplul a fost precedat de următorul meu raționament asupra subiectului ...

In primul rand:
CEL MAI IMPORTANT regulă a programării multithread în Delphi este:
În contextul unui thread non-principal, nu puteți accesa proprietățile și metodele formularelor și, într-adevăr, toate componentele care „cresc” din tWinControl.

Aceasta înseamnă (oarecum simplificat) că nici în metoda Execute moștenită de la TThread, nici în alte metode / proceduri / funcții apelate de la Execute, este interzis accesați direct orice proprietăți și metode ale componentelor vizuale.

Cum se face bine.
Nu există rețete uniforme. Mai exact, există atât de multe și diferite opțiuni pe care, în funcție de caz, trebuie să le alegeți. Prin urmare, se referă la articol. După ce a citit-o și a înțeles-o, programatorul va putea să înțeleagă și cum să o facă cel mai bine într-un anumit caz.

Pe scurt pe degete:

Cel mai adesea, o aplicație multithread devine fie atunci când este necesar să efectuați un fel de muncă pe termen lung, fie când este posibil să faceți simultan mai multe lucruri care nu încarcă puternic procesorul.

În primul caz, implementarea muncii în interiorul firului principal duce la o „încetinire” a interfeței cu utilizatorul - în timp ce se lucrează, bucla de mesaje nu este executată. Ca urmare, programul nu răspunde la acțiunile utilizatorului, iar formularul nu este desenat, de exemplu, după ce utilizatorul îl mută.

În cel de-al doilea caz, când lucrarea implică un schimb activ cu lumea exterioară, atunci în timpul „perioadelor de nefuncționare” forțate. În timp ce așteptați primirea / trimiterea datelor, puteți face altceva în paralel, de exemplu, din nou, trimite / primi date.

Există și alte cazuri, dar mai rar. Cu toate acestea, nu contează. Acum nu este vorba despre asta.

Acum, cum este scris totul. Bineînțeles, este luat în considerare un anumit caz cel mai frecvent, oarecum generalizat. Asa de.

Lucrarea desfășurată într-un fir separat, în general, are patru entități (nu știu cum să o numesc mai precis):
1. Date inițiale
2. De fapt lucrarea în sine (poate depinde de datele inițiale)
3. Date intermediare (de exemplu, informații despre starea actuală de execuție a lucrărilor)
4. Date de ieșire (rezultat)

Cel mai adesea, componentele vizuale sunt utilizate pentru a citi și afișa majoritatea datelor. Dar, după cum sa menționat mai sus, nu puteți accesa direct componentele vizuale din flux. Cum să fii?
Dezvoltatorii Delphi sugerează utilizarea metodei Synchronize din clasa TThread. Aici nu voi descrie cum să-l folosesc - există articolul menționat mai sus pentru asta. Permiteți-mi să spun doar că aplicarea sa, chiar și cea corectă, nu este întotdeauna justificată. Există două probleme:

În primul rând, corpul unei metode numite prin sincronizare este întotdeauna executat în contextul firului principal și, prin urmare, în timp ce se execută, din nou, bucla mesajului ferestrei nu este executată. Prin urmare, trebuie executat rapid, în caz contrar, vom avea aceleași probleme ca și cu o implementare cu un singur fir. În mod ideal, o metodă numită prin sincronizare ar trebui utilizată în general numai pentru a accesa proprietăți și metode ale obiectelor vizuale.

În al doilea rând, executarea unei metode prin Sincronizare este o plăcere „costisitoare” datorită necesității a două comutări între fire.

Mai mult, ambele probleme sunt interconectate și provoacă o contradicție: pe de o parte, pentru a rezolva prima, este necesar să „șlefuim” metodele apelate prin sincronizare, iar pe de altă parte, acestea trebuie adesea numite, pierzând prețioase resurse procesor.

Prin urmare, ca întotdeauna, este necesar să abordăm în mod rezonabil și, pentru diferite cazuri, să folosim diferite moduri de interacțiune a fluxului cu lumea exterioară:

Date inițiale
Toate datele care sunt transferate în flux și care nu se modifică în timpul funcționării acestuia, trebuie transferate chiar înainte de a începe, adică la crearea unui flux. Pentru a le utiliza în corpul unui fir, trebuie să faceți o copie locală a acestora (de obicei în câmpurile descendentului TThread).
Dacă există date inițiale care se pot modifica în timp ce firul rulează, atunci aceste date trebuie accesate fie prin metode sincronizate (metode numite prin sincronizare), fie prin câmpurile obiectului firului (descendent al TThread). Acesta din urmă necesită o anumită precauție.

Date intermediare și de ieșire
Aici, din nou, există mai multe moduri (în ordinea preferinței mele):
- Metoda de trimitere asincronă a mesajelor în fereastra principală a aplicației.
Se folosește de obicei pentru a trimite mesaje despre progresul procesului către fereastra principală a aplicației, cu transferul unei cantități mici de date (de exemplu, procentul de finalizare)
- Metoda de trimitere sincronă a mesajelor în fereastra principală a aplicației.
Utilizat de obicei în aceleași scopuri ca trimiterea asincronă, dar vă permite să transferați o cantitate mai mare de date, fără a crea o copie separată.
- Metode sincronizate, dacă este posibil, combinând transferul cât mai multor date într-o singură metodă.
Poate fi folosit și pentru preluarea datelor dintr-un formular.
- Prin câmpurile obiectului flux, oferind acces reciproc exclusiv.
Mai multe detalii găsiți în articol.

Eh. Nu a funcționat pentru scurt timp