Exemplu de dezvoltare a unui server simplu de rețea multi-threaded Partea 5

Acest conținut face parte din serie: Exemplu de dezvoltare a unui server simplu de rețea multietajat

Aveți grijă de articole noi din această serie.

După cunoașterea în partea anterioară a ciclului cu organizarea conexiunilor de rețea prin TCP este logic să se gândească la răspunsul programului la cererile de rețea. În stadiul actual de dezvoltare a abordărilor tehnologice, ca de obicei, câteva, ceea ce ne permite să creăm programe flexibile care sunt în proporție bună cu utilizarea și sarcina destinate. În mod tradițional, nu există o abordare mai bună, este relevantă în fiecare caz. Modurile disponibile de răspuns la cererile primite pot fi combinate, de exemplu, în următoarea listă:







  • "doar" procesează cererea curentă și se întoarce la așteptarea unei noi cereri;
  • generează un nou proces și îi transmite cererea; revenirea la așteptare;
  • generează un nou fir de execuție (thread) și trimite procesarea solicitării către el; revenirea la așteptare;
  • este posibil să se genereze mai multe procese sau fire în prealabil și să se păstreze "aburit" și atunci când se solicită transferarea procesării către unul dintre ele și revenirea la așteptare;
  • puteți utiliza combinații arbitrare de proces-flux, în care sunt generate mai multe procese care conțin mai multe fire.

Procesare simplă

Opțiunea cea mai simplă este să nu se genereze nimic, ci să se proceseze cererea în contextul aceluiași proces (sau fir) pe care așteaptă cererile. Din punctul de vedere al programării, abordarea este extrem de simplă, dar, ca o mare parte din simplă, inflexibilă și ineficientă la sarcini serioase.

Dacă cererile vin mai des decât programul are timp să le proceseze, atunci trebuie să aștepte rândul lor. Dacă coada de cereri este mai lungă decât o anumită valoare, kernelul va elimina din nou cererile primite. Este clar că într-un sistem uniprocesor cu procesor cu un singur proces, procesarea paralelă pe procesele / firele generate nu poate îmbunătăți radical situația, deoarece procesor și astfel aproape tot timpul va fi ocupat executând codul programului nostru. Cu toate acestea, prezența de procesor multi-core și / sau un sistem multi-procesor este diferența dintre prelucrare simplă și parallelized nu va fi în favoarea unei opțiuni de simplu, pentru că în acest caz va fi utilizat intensiv de un singur procesor (sau single-core), și toți ceilalți vor fi inactiv.

tratament multi-proces

În acest caz, după primirea cererii, se generează un nou proces (care este o copie a procesului curent care a primit cererea), în care se efectuează procesarea. Acest lucru se face, de exemplu, astfel (aici doar o ilustrare a unei posibile abordări):

Un nou proces este generat folosind funcția furcă (2). În acest caz, nu este nevoie să transferați în mod explicit ceva copilului, deoarece în momentul producerii icrelor, copilul este o copie a părintelui (descriptorii conexiunii se "multiplică"). După ce ați reușit să reproduceți un copil, puteți închide noua conexiune în contextul părintelui și puteți reveni la următoarea conexiune. Înainte de aceasta, în contextul părintelui stă ca ceva să-și amintească ID-ul (BIP) procesul copil mai târziu, după finalizarea acestuia, acest identificator ar putea elimina informațiile descendent încheiat proceselor din tabel, pentru a evita aglomereze stand ei și că activitățile în procesul de sistem de inițializare. Cum exact știm despre terminarea puilor?

Când starea procesului copil (suspendat, reluat sau finalizat) trimite un semnal SIGCHLD miez mamă care pot fi prelucrate în contextul părinte și să ia măsurile corespunzătoare. Procesarea acestui semnal trebuie inclusă în mod explicit, deoarece în mod implicit este ignorată. La sosirea semnalului, puteți rula o listă a descendenților salvați pid, pentru fiecare apel, de exemplu, funcția waitpid (2) în modul de blocare și examinați rezultatul. Puteți evita această listă enumerare pentru părinții copiilor (înainte de terminarea) pid acest descendent prin oricare dintre metodele de IPC (Inter Process Communication) - canal (conducte), fișierul soclu sau razdelyamuyu memorie. În acest caz, există o "oportunitate" de a obține mai multe pid simultan ca urmare a finalizării aproape simultane a mai multor descendenți, din cauza căruia mai multe semnale se pot "îmbina" într-una. Cu toate acestea, în cazul în care aplicația dvs. rulează simultan cu mulți descendenți, chiar și o astfel de primire "de lot" de pid este preferabilă să enumerăm întreaga listă pentru fiecare apariție a semnalului. Un efect secundar al utilizării proceselor de paralelizare a procesării este independența relativă a acestei procesări între procese, deoarece fiecare proces are propriul spațiu de memorie izolat și propriile sale erori. Toate acestea înseamnă că un eșec critic într-unul dintre procese de obicei nu duce la prăbușirea întregului program. În funcție de preferințele și experiența proceselor de memorie izolarea dvs. poate fi un minus, pentru că datorită acesteia în procesele inițial nu conține variabile partajate sau unele mai multe structuri de date, precum și pentru schimbul de informații între ele vor trebui să exercite un efort suplimentar pentru a organiza cu metodele IPC. Acest lucru va necesita atenție și sincronizare (pentru a ocoli problema "scriitorilor" și "cititorilor"), astfel încât datele dintr-un astfel de schimb să nu se stingă. O soluție simplă la această problemă poate fi utilizarea țevilor, dar și limitările lor. Cel mai productiv cu volume semnificative (sute de kilobytes sau mai mult) este ca schimbul de date non-sincron prin memorie partajată, dar cu condiția ca datele sunt utilizate de locație de stocare, fără copie suplimentară a memoriei partajate în altă parte, iar acest lucru va necesita un design atent și exacte și, cel mai probabil, complica aplicația.







Procesarea cu mai multe fire

În prezent, în GNU / Linux, firele diferă de procese în principal dintr-un set de proprietăți și lucruri cum ar fi funcțiile de apelare ale familiei execve (2), exec (3) într-unul din fire. Ie procesele și firele sunt obiecte de programare pentru planificatorul kernel-ului, pot fi blocate independent, recepționează semnale etc. De fapt, chiar și procesul de copii și fluxul are loc într-un mod similar utilizând funcția clona (2) enumeră steagurile relevante care determină proprietățile obiectului generat (detalii pot fi găsite în manualul pentru această funcție).

Cu toate acestea, acestea sunt încă detalii ale unei implementări specifice, iar relația dintre procese și fluxuri a rămas aceeași:

Este posibil să reacționați la conexiunea de intrare prin generarea unui flux, de exemplu, în acest fel (aceasta este și o ilustrație, nu un cod complet):

Un nou fir de execuție este creat folosind pthread_create (3), care, dacă reușește, plasează identificatorul firului generat în memorie la cursorul threadId. Printre argumentele funcției pthread_create (3) este una (cu tipul void *) care transmite ceva la funcția de flux (care este punctul de la începutul firului). Puteți trece o singură variabilă (de exemplu, descriptorul socket-ului cu noua conexiune în cazul nostru), mai întâi să-l conducem în mod explicit la tipul void *, și apoi să facem turnarea inversă deja în funcția flux. Dacă doriți să transferați date bloc (inclusiv "eterogene"), atunci este trecut un indicator la acest bloc cu aceeași distribuție explicită de două ori.

Programarea Multithreaded este un exercițiu bun în parametru, deoarece nu este de obicei detectate toate fluxul necesar de date externe sunt combinate într-o singură structură care este umplută, înainte de curgere și un pointer la un produs care este apoi trecut la fluxul de curgere ca funcție argument. Datorită acestui punct de intrare în fluxul de multe ori este singurul punct de date în acest thread (cu excepția sumei minime a ceea ce unii globale de date) în mod direct sau indirect (prin indicatori), care este util pentru motive de simplitate și ordine.

Domeniul de aplicare global conține, de obicei, date pe care toate firele necesită în același timp și care nu ar trebui să fie schimbate sau date care fac obiectul schimburilor între fire. Obiectele globale pot de asemenea să declare mutexuri și semaphore, dar acest lucru este probabil mai determinat de preferințele personale - folosesc numele "vorbind" pentru munca mea și de obicei le declar în domeniul global.

În mod separat, ar trebui să ne oprim la sfârșitul fluxului. În acest context, firele pot fi de două tipuri - atașate (atașate) și nu atașate (deconectate, deconectate etc.). Primele mijloace de curgere aproximativ aceeași ca și pentru procesul - după finalizarea copilului flux-mamă trebuie să „pună“ în spatele lui prin mijloace pthread_join (3) funcții, care au nevoie pentru a stoca și ID-ul curent (threadId în exemplu). Daca nu sunteti interesat de funcția de retur a valorii sau nu returnează nimic, puteți crea un flux imediat deconectat și după ce nu va mai fi nimic pentru a menține sistemul și, în consecință, să-și petreacă resursele.

Un punct important în dezvoltarea aplicațiilor multi-thread este siguranța firelor funcțiilor numite în fluxuri: securitate sau fiabilitate în timpul executării unei funcții în contextul mai multor fire din același proces. Acest lucru poate fi realizat, de exemplu, prin intermediul secțiunii critice deja menționate mai sus și / sau a altor metode:

  • povtornovhodimost (re-entrancy): În cazul general, este imposibil de prezis cum se va planifica procesele de lucru și programatorul fir de nucleu (în ce ordine, la ce oră), și se poate întâmpla ca un fir va fi suspendat în timpul executării codului unor funcții și de management va fi trecut la un alt fir, care va efectua aceeași funcție, și apoi va fi returnat înapoi la firul suspendat. Repetabile sunt funcțiile, atunci când se execută un cod ale cărui fire nu observă o astfel de "substituire". Pe de altă parte, funcțiile neoriginale pot fi folosite ca "indicatori ai faptelor de planificare" (deși nu cu răspunsul de 100%). Repetabilitatea poate fi realizată utilizând numai variabilele locale și excluzând lucrul cu variabilele globale asupra schimbării.
  • stocare locală: înainte de a lucra cu toate datele necesare deja în contextul fluxului, trebuie mai întâi să faceți copiile lor în variabile locale pentru funcția de curgere, care pentru fiecare fir sunt proprii.
  • atomicitate: există o serie de operații în care kernelul garantează continuitatea, adică dacă sunt pornite, acestea vor fi completate neapărat în contextul firului curent fără suspendarea lui; ca regulă, acestea sunt operații elementare cum ar fi creșterea / scăderea.

Proces-flux combinații

Este unirea celor două metode de mai sus-menționate, se evită, de obicei, procesul de generare a fluxurilor - de ce aveți nevoie de o copie a procesului, cu o grămadă de a face propriile lor fluxuri de lucru? In schimb, fie toate procesele necesare sunt generate în prealabil, pentru a crea compuși și fluxuri care generează sau a generat „între ori“ după cum este necesar (de exemplu pentru umplerea decalată marja de proces „aburit“ atunci când epuizat) dintr-o separat, „exemplar“ și / sau proces "pur". Această abordare este justificată în situațiile în care timpul petrecut pentru generarea unui proces sau flux pentru prelucrarea unei conexiuni este important și trebuie redus la minim. Dezvoltarea unor astfel de programe este o sarcină destul de dificilă, în principal datorită nevoii de a gândi cu grijă și de a stabili o comunicare interproces și interflux. De asemenea, ar putea fi necesar să se dezvolte anumite comenzi și / sau proceduri interne pentru o interacțiune mai flexibilă și controlată a proceselor și fluxurilor între ele.

concluzie

O abordare mixtă poate fi aplicată atunci când este necesar să se combine ambele opțiuni menționate într-o singură cerere.

De asemenea, articolul nu menționează problema anulării fluxurilor. În plus, această parte, ca și altele, implică încă o cantitate semnificativă de muncă independentă.

Descărcați resurse

Subiecte conexe

  • Un exemplu de dezvoltare a unui server de rețea cu mai multe filete, cu suport pentru sesiuni de utilizatori în C în GNU / Linux: Partea 1. Familiarizarea cu mediul de dezvoltare. Analiza parametrilor liniei de comandă, suportul ajutorului încorporat, "demonizarea" programului.
  • Un exemplu de dezvoltare a unui server de rețea simplu multi-threaded cu suport pentru sesiuni de utilizatori în limba C în sistemul de operare GNU / Linux: Partea 2. Analiza completă a parametrilor liniei de comandă.
  • Exemplu de dezvoltare a unui server de rețea simplu cu mai multe fire, cu suport pentru sesiuni de utilizatori în C în GNU / Linux: Partea 3. Lucrul cu fișierul de configurare, inițializarea structurilor interne ale programului.
  • Un exemplu de dezvoltare a unui server de rețea simplu cu multe fire, cu suport pentru sesiuni de utilizatori în C în GNU / Linux: Partea 4. O prezentare generală a tehnicilor I / O atunci când este aplicată conexiunilor de rețea.
  • Un exemplu de dezvoltare a unui server web simplu multifir cu suport pentru sesiunile de utilizator în limba C în sistemul de operare GNU / Linux: Partea 5: Metode de procesare paralelă a cererilor de rețea (procese, fire și combinații ale acestora).
  • Un exemplu de dezvoltare a unui server de rețea simplu multi-threaded cu suport pentru sesiuni de utilizatori în C în sistemul de operare GNU / Linux: Partea 6: Mecanisme de autentificare.






Trimiteți-le prietenilor: