De ce avem nevoie de concurență

Odată, a existat un timp vechi bun când viteza ceasului s-a dublat la fiecare 18 luni. Acest fenomen a fost numit legea lui Moore. Dacă programul unui programator nu era suficient de rapid, ar putea aștepta și în curând computerele vor ajunge din urmă.

A fost prea bine pentru a rezista și nu a durat. Proiectanții CPU continuă să țină pasul cu Legea lui Moore adăugând mai multe nuclee computerelor.

Acest lucru a creat o problemă pentru programatori. În lumea nouă, programele noastre vor rula de două ori mai repede la fiecare 18 luni, dar numai dacă este un program paralel care folosește mai mult nucleu.

Prin urmare, pentru un programator, capacitatea de a scrie cod în medii paralele este o abilitate critică. Această postare explorează modul în care diferite limbaje de programare acceptă programe paralele și concurente.

Primitive concurente clasice

Aproape toate sistemele de operare acceptă mai multe fire de execuție. Cu toate acestea, programatorii simultani au nevoie de ajutor pentru a rezolva alte două probleme.

  • Date partajate – Datele partajate, dacă sunt accesate simultan, pot produce rezultate neașteptate.
  • Semnalizarea între fire – Unele cazuri de utilizare au nevoie de programatori pentru a controla ordinea de execuție a firelor. Alte exemple sunt ca dorirea firelor să aștepte într-un anumit moment, să aștepte un alt fir, să ruleze în ordine specifică, să nu depășească niciodată un alt fir și să nu aibă mai mult de N fire în regiunea critică.

Limbajele de programare oferă diferite primitive pentru a ajuta programatorii să controleze situațiile de mai sus. Să aruncăm o privire asupra acelor primitive clasice:

  1. Blocări (numite și Mutex) – asigurați-vă că este executat un singur fir în anumite regiuni ale codului
  2. Monitoare – fac același lucru, dar puțin mai bine decât încuietorile, deoarece te obligă să deblochezi
  3. (Numărare) Semafore – abstracții puternice care pot susține o gamă largă de scenarii de coordonare
  4. Așteptați și notificați – face la fel, dar este mai slab decât semaforele. Programatorul trebuie să gestioneze declanșatoarele de notificări ratate înainte de așteptare
  5. Variabile condiționale – lăsați un fir să doarmă și să se trezească atunci când apare o anumită afecțiune
  6. Canale și buffere cu așteptare condiționată – ascultați și colectați mesaje dacă nu există niciun fir de recepție (cu buffere delimitate opțional)
  7. Structuri de date care nu blochează (cum ar fi coada de nonblocare, contoare atomice) – Acestea sunt structuri de date inteligente care permit accesul din mai multe fire fără a utiliza blocări sau o cantitate minimă de blocări.

Acești primitivi se suprapun asupra a ceea ce pot face. Orice limbaj de programare poate obține toată puterea concurenței cu doar câteva. De exemplu, încuietorile și semaforele pot face orice caz de utilizare simultană pe care vi-l puteți imagina.

Suport lingvistic pentru primitive

Primitivitatea simultană nu este selectată doar pentru puterea sa. Diferite primitive au modele de programare diferite. Acest lucru necesită diferite moduri de a gândi problema. Diferite limbaje de programare au selectat diferite subseturi care se potrivesc cel mai bine modelului lor de limbă. Alegerea depinde de gusturile designerului, precum și de filosofia limbajului.

Să explorăm câteva dintre aceste alegeri.

Java și C #

Java și C # au ales să nu aleagă deloc. Ambele susțin toate primitivele.

Java a început mai întâi doar suportând monitoare ( sincronizat cuvânt cheie) și așteptați și notificați. A fost un coșmar să trimitem semnale prin fire. Îmi amintesc că am petrecut ore întregi pe „semnal pierdut” și totuși am greșit.

Curând, designerii Java și-au dat seama de greșeala lor. Au adăugat un pachet de concurență care are totul, inclusiv structuri de date care nu blochează.

Singura primitivă neacceptată în formele sale pure este canalele și tampoanele. Cu toate acestea, dacă le doriți, este ușor să imitați canalele cu cozi și tampoane. Deși implementarea dvs. nu s-ar potrivi niciodată cu Go sau Erlang ca performanță.

C #, venind târziu, a învățat din Java. De asemenea, are cam totul. C # are, de asemenea, câteva construcții de ajutor de nivel superior pe care Java nu le are. Aceasta rezolvă probleme comune, cum ar fi barierele. Pentru mai multe detalii, consultați Pachet de filetare C #.

C și C ++

C depindea inițial de apelurile sistemului de operare pentru a face multithreading. Pe atunci codul nu era portabil. În schimb, bibliotecile de concurență terțe au furnizat această funcționalitate. Din păcate, deoarece limbajul nu fixează API-ul, au existat multe biblioteci disponibile.

Deoarece C și C ++ sunt limbaje cele mai apropiate de sistemul de operare, cercetarea firului de ultimă generație se face adesea cu aceste două limbi. De exemplu, a fost dezvăluită o căutare rapidă 22 concurență C ++ biblioteci și 6 biblioteci simultane C. Nu lipsește puterea.

Aceste biblioteci oferă o gamă largă și o tehnologie de ultimă generație. Cu toate acestea, datorită diversității API-urilor, nu există mulți programatori care să fie la fel de competenți cu un API dat.

Erlang

Erlang a fost conceput de la zero pentru concurență. Erlang oferă programatorului control deplin al interacțiunilor dintre fire. Programatorii fac toate comunicările prin transmiterea mesajelor. Aceasta este sursa performanței legendare a Erlang pe computerele multi-core.

Cu toate acestea, există un preț de plătit. Erlang nu acceptă partajarea stării între fire. Nu este o greșeală. Starea partajată declanșează sincronizarea între fire, care nu va fi sub controlul direct al programatorului. O astfel de sincronizare reduce adesea performanța.

În consecință, experiența de programare Erlang este străină pentru majoritatea programatorilor. Nici natura sa complet funcțională nu ajută.

Construcția principală simultană în Erlang este canalele. Include tampoane și suport pentru așteptarea unei condiții. De exemplu, puteți cere unui canal să aștepte până când acesta primește un mesaj care îndeplinește o anumită condiție. Fiecare proces are un singur canal și poate primi doar de la acel canal.

În practică, deoarece Erlang este un limbaj de programare funcțional, blocajele de memorie partajată sunt rareori necesare. Din păcate, astfel de cazuri de utilizare există. Deoarece Erlang nu are memorie partajată, nu puteți bloca ceva. Cu toate acestea, puteți crea un proces pentru a reprezenta o blocare. Achiziționați și eliberați o blocare prin trimiterea de mesaje la blocare la fel ca într-un sistem distribuit.

Cu excepția cazului în care sunteți un expert în limbaje de programare, care cunoaște intim programarea funcțională, programele rezultate tind să fie adesea complicate și greu de depanat. Alegând Erlang, programatorii schimbă suportul și familiaritatea simultană.

Dacă doriți să aflați mai multe, citiți aceste articole: Erlang pentru programare concurentă și Ghidul autostopistului pentru concurență.

Merge

Go seamănă mult cu Erlang. Modul său principal de concurență este prin canal și buffere și acceptă așteptarea condiționată. Filozofia sa de bază pentru concurență este: Nu comunicați prin partajarea memoriei; în schimb, partajați memoria comunicând.

Există, totuși, o diferență fundamentală. Go are încredere în tine să faci ceea ce trebuie. Go, permiteți-vă să partajați date între fire și acceptă ambele mutexuri și semafor. Mai mult, au relaxat restricția Erlang conform căreia fiecare canal este atribuit permanent unui thread. Puteți crea un canal și îl puteți transmite.

În rezumat, Go dorește să programăm concurență precum Erlang. Cu toate acestea, în timp ce Erlang o aplică, Go are încredere în tine să faci ceea ce trebuie. Dacă Erlang este draconian, Go este un stat liber.

Rugini

Rugina seamănă și cu Erlang și Go. Comunică utilizând canale care au tampoane și așteptare condiționată. La fel ca Go, relaxează restricțiile din Erlang lăsându-vă să faceți memorie partajată, susținând numărarea și blocarea referințelor atomice și permițându-vă să treceți canale de la fir la fir.

Cu toate acestea, Rust face un pas mai departe. În timp ce Go are încredere în tine să faci ceea ce trebuie, Rust îi atribuie un mentor care stă cu tine și se plânge dacă încerci să faci un lucru greșit. Mentorul lui Rust este compilatorul. Face analize sofisticate pentru a determina proprietatea asupra valorilor care sunt transmise în jurul firelor și oferă erori de compilare dacă există potențiale probleme.

Urmează un citat din documentele Rust.

Regulile de proprietate joacă un rol vital în transmiterea mesajelor, deoarece ne ajută să scriem un cod sigur și concomitent. Prevenirea erorilor în programarea simultană este avantajul pe care îl obținem făcând compromisul de a trebui să ne gândim la proprietate în cadrul programelor noastre Rust. – Transmiterea mesajului cu proprietatea asupra valorilor.

Dacă Erlang este draconian și Go este un stat liber, atunci Rugina este o stare de bonă.

Depanarea programelor concurente este un coșmar. Într-o zi proastă, poate dura zile. Deci, apreciez ceea ce încearcă Rust să facă prin analiza nivelului compilatorului.

Cu toate acestea, dacă nu aveți experiență în concurență și încercați să scrieți un program Rust simultan, vă va enerva. Orice ai face, se va plânge de concurența în limbaj criptic. Când vă schimbați, va spune altceva și apoi din nou. Până când nu înțelegeți concurența în detaliu, nu va fi ușor.

În schimb, Go oferă o securitate falsă programatorului, care crede că sarcina lor, adesea falsă, este realizată. S-ar putea să plătească pentru asta mai târziu. Cu toate acestea, vor plăti numai dacă codul ajunge vreodată la producție, dacă utilizatorul întâlnește scenariul și dacă este detectată acea eroare. Aceasta este o mulțime de „dacă”. Deși este nedrept, șansele sunt că programatorul ar putea scăpa de el. Oamenii nu sunt atât de buni cu satisfacție întârziată si perspectivă oricum. Așadar, programatorii preferă adesea Go over Rust.

Rugina încearcă să ajute, dar rareori ajutorul este apreciat. Nimănui nu îi place o stare de bonă.

Rugina nu este atât de populară pe cât merită să fie, pentru că prea mulți dezvoltatori miopi sunt deranjați de strictețea lui Rust, în loc să aprecieze puterea imensă pe care o câștigă din acea strictețe. ” – rjc2013

Pentru mai multe informații, vă rugăm să citiți Cum se compară primitivele simultane din Rust cu cele din Go?

Concluzie

Când vine vorba de ideologii simultane, limbajele de programare vă oferă o alegere: un stat liber (Go), un stat draconian (Erlang) sau un stat de bonă (Rust).

Dacă doriți să aflați mai multe, aș recomanda două resurse.

Mai întâi, citiți fișierul Micuță carte de semafor, care vă învață totul despre încuietori și semafore.

În al doilea rând, dacă doriți să înțelegeți canalele și modelul Erlang, verificați MPI. S-ar putea crede că MPI este un limbaj mort. Nu este. Majoritatea simulărilor științifice se fac până în prezent cu MPI. Vremea este prezisă de aceasta, vehiculele sunt proiectate cu ea și drogurile sunt descoperite cu ea. Știința progresează literalmente folosind MPI. MPI folosește concurența în moduri pe care nu ni le-am putea imagina niciodată. Pentru un gust, vă rugăm să verificați Primitive de comunicare MPI.

Dacă urmați cele două sugestii de mai sus, veți pleca cu o apreciere a complexității și a posibilităților concurenței. Este un subiect care necesită o viață pentru a stăpâni.

Sper că acest articol a fost util. Am studiat aceste limbi în timp ce mă gândeam la un model de concurență pentru Ballerina. Ballerina este un nou limbaj de programare conceput pentru medii distribuite pentru a scrie microservicii și pentru a integra API-uri. Include noi funcții de concurență, cum ar fi blocarea adaptivă. Acesta analizează codul și încearcă să țină blocările cât mai scurt timp posibil. Verificați-l la https://ballerina.io.