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] .
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ă:
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.
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).
Cazuri în care utilizarea spinlock-urilor în spațiul utilizatorului dă un efect tangibil:
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:
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.
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ă.