Blocare de rotire

Versiunea actuală a paginii nu a fost încă examinată de colaboratori experimentați și poate diferi semnificativ de versiunea revizuită la 7 august 2022; verificările necesită 3 modificări .

Un spin lock sau spinlock ( în engleză  spinlock - cyclic lock) este o primitivă de sincronizare de nivel scăzut [1] utilizată în sistemele multiprocesor pentru a implementa excluderea reciprocă a execuției secțiunilor critice de cod folosind o buclă de așteptare activă [2] . Este folosit în cazurile în care așteptarea unei blocări este de așteptat să fie scurtă [2] sau dacă contextul de execuție nu permite trecerea la o stare blocată [3] .

Spinlock-urile sunt similare cu mutexurile , permițându-vă să petreceți mai puțin timp blocând un fir, deoarece nu este necesar să transferați firul în starea blocată. În cazul mutexurilor, poate fi necesar să invocați planificatorul pentru a schimba starea firului de execuție și a-l adăuga la lista de fire care așteaptă să fie deblocate. Spinlock-urile nu folosesc planificatorul și folosesc o buclă de așteptare fără a schimba starea firului de execuție, ceea ce pierde timp CPU așteptând ca un alt fir să elibereze blocarea. O implementare tipică a unui spinlock este o simplă verificare ciclică a variabilei spinlock pentru disponibilitate [1] .

Implementare fizică

Din punct de vedere fizic, un spinlock este o variabilă în memorie și este implementat pe operațiuni atomice care trebuie să fie prezente în setul de instrucțiuni al procesorului . Fiecare procesor care dorește să acceseze resursa partajată scrie atomic valoarea condiționată „ ocupat ” la această variabilă, folosind un analog al operației de swap (în arhitectura x86 - xchg). Dacă valoarea anterioară a variabilei (returnată de comandă) a fost „ liberă ”, atunci procesorul dat este considerat că a accesat resursa, în caz contrar procesorul revine la operația de swap și trece prin spinlock până când este eliberat. După ce a lucrat cu o resursă partajată, procesorul - proprietarul spinlock-ului trebuie să scrie în ea valoarea condiționată „ free ”.

Un exemplu de implementare a unui spinlock în asamblatorul x86:

mov eax , spinlock_address mov ebx , SPINLOCK_BUSY ciclu_așteptați: xchg [ eax ], ebx ; xchg este singura instrucțiune care este atomică fără prefixul de blocare cmp ebx , SPINLOCK_FREE jnz wait_cycle ; <secțiunea critică este capturată de acest fir, lucrul cu resursa partajată este în desfășurare aici> mov eax , spinlock_address mov ebx , SPINLOCK_FREE xchg [ eax ], ebx ; utilizați xchg pentru schimbarea atomică ; ultimele 3 instrucțiuni ar trebui înlocuite cu mov [spinlock_address], SPINLOCK_FREE - ; aceasta va crește viteza din cauza absenței blocării inutile a magistralei, iar mov va fi executat oricum atomic ; (dar numai dacă spinlock_address este aliniat pe o graniță dword)

O implementare mai inteligentă ar folosi o operațiune obișnuită, mai degrabă decât o operațiune atomică pentru sondarea într-o buclă și o operație atomică doar pentru încercările de captare. Cert este că implementarea operațiunilor de memorie atomică are loc prin blocarea hardware a magistralei de sistem de către procesor pe durata operațiunii atomice (care include citirea, modificarea și scrierea). În timpul acestor trei operațiuni, nu pot fi efectuate alte operațiuni pe magistrală, ceea ce reduce performanța altor procesoare din sistem (dacă au o magistrală comună ), chiar dacă nu au nicio legătură cu acest spinlock.

De asemenea, sunt folosite așa-numitele. spinlocks în coadă - „spinlocks în coadă”. În loc să atribuie 0 sau 1 unei variabile atomice, ei folosesc o adăugare atomică a unei structuri la capul listei, în timp ce capul listei este o variabilă atomică de tip „pointer”.

Proprietăți utile ale spinlock-urilor în coadă:

  • garanție a ordinii de furnizare în ordinea cererii, o garanție împotriva „foamei”
  • în bucla de sondare, fiecare procesor își interogează variabila locală
  • exact 1 operatiune atomica la captare si exact 1 la eliberare

Spinlock-urile sunt folosite pentru a sincroniza mici secțiuni de cod atunci când utilizarea unor mecanisme mai complexe este nerezonabilă sau imposibilă. Implementarea primitivelor de sincronizare și a managerului de fire necesită în mod necesar blocări pentru a proteja listele de fire care sunt gata de executare și listele de fire care așteaptă obiecte. O astfel de blocare poate fi doar un spinlock datorită nivelului său foarte scăzut. Astfel, spinlock-ul este cea mai joasă primitivă de sincronizare pe care se bazează implementarea tuturor celorlalte.

Versiunile de Windows din Windows 7 inclusiv utilizează paradigma structurilor de date fără blocare pentru a implementa dispecerul/programatorul. Astfel, ei sunt scutiți de singurul spinlock global KiDispatcherLock, unul dintre cele mai încărcate din nucleul sistemului de operare.

Specificul configurațiilor multiprocesor și uniprocesor

Există o opinie larg răspândită că, în aplicațiile utilizator care rulează sub sistemul de operare multitasking, utilizarea spinlock-urilor este inacceptabilă, deoarece așteptarea eliberării unui spinlock duce la o așteptare activă într-o buclă care irosește resursele de calcul ale CPU, iar primitivele de nivel înalt trebuie să fie folosit pentru a sincroniza programele utilizatorului, care implică așteptare pasivă - dacă un anumit fir de execuție nu poate continua execuția, atunci dă control sistemului de operare și nu se rotește într-o buclă de așteptare spinlock (care poate fi infinită). De fapt, această afirmație este 100% adevărată numai pentru sistemele uniprocesor. În multe cazuri, utilizarea spinlock-urilor în configurațiile SMP duce la câștiguri de eficiență dacă sondarea și obținerea unui spinlock este mai rapidă decât apelarea unei achiziții mutex în nucleu.

Principalul criteriu aici este contenția - „rigiditatea” competiției pentru resursă. O resursă încărcată ușor, care nu este un site de execuție popular, se comportă diferit de o resursă încărcată puternic, care este capturată și dealocată foarte des.

În plus, în același Windows, există varietăți de mutex (de exemplu, binecunoscutul CRITICAL_SECTION/EnterCriticalSection/LeaveCriticalSection, sau sinonimul acestuia în kernel-ul OS - FAST_MUTEX/ExAcquireFastMutex/ExReleaseFastMutex), care funcționează mai întâi ca un spinlock, folosind un sondaj de valoare în memorie și numai apoi, după un număr mare de sondaje, mergeți la kernel pentru a bloca așteptarea. Astfel de obiecte combină cele mai bune calități ale spinlock-urilor (costul minim de captură) și mutex (fără risipă de resurse CPU pentru sondare).

Utilizarea spinlock-urilor

Cazuri în care utilizarea spinlock-urilor în spațiul utilizatorului dă un efect tangibil:

  • În interiorul secțiunii codului protejat există mai multe variabile asociate, al căror timp de modificare poate fi de sute și chiar de mii de ori mai mic decât o comutare de context de către procesor, ceea ce este o operațiune deosebit de costisitoare, mai ales pe sistemele moderne.
  • Blocarea nu a secțiunilor de cod , ci a datelor (cu fiecare structură de date care trebuie schimbată atomic ca un întreg, este asociat un spinlock care o protejează)
  • Optimizarea codului atunci când este necesar să se reducă sarcina care apare din cauza comutării prea frecvente a contextului

Cu toate acestea, utilizarea „mutexurilor rapide” precum CRITICAL_SECTION din Win32 face ca toate cele de mai sus să nu fie necesare în spațiul utilizatorului.

Cazuri în care utilizarea spinlock-urilor nu este justificată și este o risipă de resurse ale procesorului:

  • Operațiuni lungi de blocare în interiorul secțiunii de cod protejat (I/O pe disc și rețea pot dura foarte mult timp conform standardelor de procesor)
  • Configurații cu un singur procesor - procesorul petrece restul intervalului de timp într-un ciclu inactiv .

Probleme Spinlock și metode de rezolvare a acestora

Pe procesoarele moderne, ciclul de repaus poate fi foarte rapid datorită particularităților arhitecturii pipeline, care, pe lângă ciclurile inactiv de înfășurare, poate duce la încălzire mai intensă decât în ​​timpul funcționării normale.

Pentium 4 și modelele ulterioare de procesoare Intel au introdus o instrucțiune specială de asamblare pentru a introduce într-o buclă de pauză ( opcode 0xf3 0x90, similar cu rep nop pentru compatibilitate cu procesoare mai vechi) care are scopul de a instrui procesorul că acest ciclu este o buclă de așteptare și permite procesorului să suporte mai multe fire de execuție pe același nucleu, trece la următorul thread.

Versiunile de Windows începând cu Windows 7 sunt optimizate pentru a rula ca „oaspete” într-o mașină virtuală și, în loc de pauză în cazurile în care sistemul de operare rulează ca invitat, un apel special „anunțați hypervisorul că suntem într-o buclă de așteptare” este folosit.

Alternative la spinlocks

  • Spinlock-urile sunt folosite pentru a se asigura că un fir are acces exclusiv la o structură de date protejată. Nu face distincție între firele în sine și nici între operațiunile efectuate. Cu toate acestea, adesea în aplicațiile reale, firele pot fi împărțite în „cititori” și „scriitori”. Pentru acest caz asimetric, este mai potrivit să folosiți blocări de citire-scriere . Structura poate fi utilizată simultan de un număr nelimitat de fire de execuție în modul „numai citire”, oferind în același timp protecție a integrității datelor atunci când sosește un fir de „scriere”.
  • Există, de asemenea, algoritmi fără blocare bazați pe detectarea coliziunilor atomice. Ele sunt optimizate pentru cazul optimist, în care întreaga verificare a coliziunii este redusă la o operație de asamblare atomică ( Compare And Swap , pe arhitectura x86 - comanda cmpxchg )

Alte modificări ale spinlock-urilor

Spinlock cu creștere automată până când un mutex cu drepturi depline este capturat după expirarea unui anumit număr de rotații de ciclu este utilizat, de exemplu, în secțiunile critice ale Windows pentru optimizare, care constă în absența apelurilor către mutex în absența concurenței pentru o resursă.

Note

  1. ↑ 1 2 IEEE, Grupul deschis. Justificare pentru interfețele de sistem , informații generale  . Specificațiile de bază ale grupului deschis numărul 7, ediția 2018 . Grupul deschis (2018). Preluat la 20 iunie 2020. Arhivat din original la 18 iunie 2020.
  2. 1 2 Tanenbaum, 2011 , 2.3.3. Active Wait Mutual, Intercalare strictă, p. 156.
  3. Oleg Tsiliurik. Instrumente de programare kernel: Partea 73. Paralelism și sincronizare. Încuietori. Partea 1 . - www.ibm.com, 2013. - 13 august. — Data accesului: 06.12.2019.

Literatură

  • M. Russinovici , D. Solomon. 1 // Interne Microsoft Windows. - Ed. a VI-a - Sankt Petersburg. : Petru, 2013. - S. 218-222. — 800 s. - ("Master-class"). — ISBN 978-5-459-01730-4 .
  • Walter Ei. Utilizarea modelului de driver Microsoft Windows . - Ed. a II-a - Sankt Petersburg. : Petru, 2007. - S.  173 -178. — 764 p. - ISBN 978-5-91180-057-4 .
  • Andrew S. Tanenbaum. Modern Operating Systems  = Modern Operating Systems. — ediția a 3-a. - Sankt Petersburg: Peter: Editura „Peter”, 2011. - S. 165-170. — 1117 p. — (Clasice ale informaticii). — ISBN 9785459007572 .

Vezi și