Verificați blocarea

Versiunea actuală a paginii nu a fost încă examinată de colaboratori experimentați și poate diferi semnificativ de versiunea revizuită pe 20 septembrie 2017; verificările necesită 7 modificări .
Verificați blocarea
Închidere dublu verificată
Descris în Design Patterns Nu

Blocarea dublu verificată este un  model de proiectare paralel conceput pentru a reduce supraîncărcarea asociată cu obținerea unei încuietori. În primul rând, se verifică starea de blocare fără nicio sincronizare; firul încearcă să obțină blocarea numai dacă rezultatul verificării indică faptul că trebuie să obțină blocarea.

Pe unele limbi și/sau pe unele mașini nu este posibilă implementarea în siguranță a acestui model. Prin urmare, uneori este numit un anti-model . Astfel de caracteristici au condus la o ordine strictă a relației „ se întâmplă înainte ” în Modelul de memorie Java și Modelul de memorie C++.

Este folosit în mod obișnuit pentru a reduce costul general al implementării inițializării leneșe în programele cu mai multe fire, cum ar fi ca parte a modelului de proiectare Singleton . În cazul inițializării leneșe a unei variabile, inițializarea este întârziată până când valoarea variabilei este necesară în calcul.

Exemplu de utilizare Java

Luați în considerare următorul cod Java luat din [1] :

// Clasa de versiune cu un singur thread Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) helper = new Helper (); ajutor de întoarcere ; } // și alți membri ai clasei... }

Acest cod nu va funcționa corect într-un program cu mai multe fire. Metoda getHelper()trebuie să dobândească o blocare în cazul în care este apelată simultan din două fire. Într-adevăr, dacă câmpul helpernu a fost încă inițializat și două fire apelează metoda în același timp getHelper(), atunci ambele fire vor încerca să creeze un obiect, ceea ce va duce la crearea unui obiect suplimentar. Această problemă este rezolvată prin utilizarea sincronizării, așa cum se arată în exemplul următor.

// Versiunea corectă, dar „scumpe” cu mai multe fire clasa Foo { private Helper helper = null ; public synchronized Helper getHelper () { if ( helper == null ) helper = new Helper (); ajutor de întoarcere ; } // și alți membri ai clasei... }

Acest cod funcționează, dar introduce o suprasarcină suplimentară de sincronizare. Primul apel getHelper()va crea obiectul și doar câteva fire care vor fi apelate getHelper()în timpul inițializării obiectului trebuie sincronizate. Odată inițializată, sincronizarea la apel getHelper()este redundantă deoarece va citi doar variabila. Deoarece sincronizarea poate reduce performanța cu un factor de 100 sau mai mult, supraîncărcarea de fiecare dată când această metodă este apelată pare inutilă: odată ce inițializarea este completă, blocarea nu mai este necesară. Mulți programatori au încercat să optimizeze acest cod astfel:

  1. În primul rând, verifică dacă variabila este inițializată (fără a obține o blocare). Dacă este inițializată, valoarea sa este returnată imediat.
  2. Primirea unui lacăt.
  3. Verifică din nou dacă variabila este inițializată, deoarece este foarte posibil ca după prima verificare, un alt thread să fi inițializat variabila. Dacă este inițializată, valoarea sa este returnată.
  4. În caz contrar, variabila este inițializată și returnată.
// Versiune cu mai multe fire incorecte (în Symantec JIT și Java versiunile 1.4 și anterioare) // Clasa de model „Blocare dublu verificată” Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) { helper = new Helper (); } } } return helper ; } // și alți membri ai clasei... }

La nivel intuitiv, acest cod pare corect. Cu toate acestea, există unele probleme (în Java 1.4 și anterioare și implementările JRE non-standard) care ar trebui probabil evitate. Imaginați-vă că evenimentele dintr-un program multi-thread decurg astfel:

  1. Thread A observă că variabila nu este inițializată, apoi dobândește blocarea și începe inițializarea.
  2. Semantica unor limbaje de programare[ ce? ] este astfel încât firul A i se permite să atribuie o referință unui obiect care se află în proces de inițializare unei variabile partajate (care, în general, încalcă destul de clar relația cauzală, deoarece programatorul a cerut destul de clar să atribuie o referință la un obiect la variabila [adică pentru a publica o referință în shared] - în momentul de după inițializare, și nu în momentul înainte de inițializare).
  3. Thread B observă că variabila este inițializată (cel puțin așa crede) și returnează valoarea variabilei fără a obține o blocare. Dacă firul B folosește acum variabila înainte ca firul A să termine inițializarea, comportamentul programului va fi incorect.

Unul dintre pericolele utilizării blocării dublu-verificate în J2SE 1.4 (și anterioare) este că programul pare să funcționeze deseori corect. În primul rând, situația luată în considerare nu va apărea foarte des; în al doilea rând, este dificil să distingem implementarea corectă a acestui tipar de cea care are problema descrisă. În funcție de compilator , de alocarea de către programator a timpului procesorului pe fire și de natura altor procese care rulează simultan, erorile cauzate de implementarea incorectă a blocării dublu verificate apar de obicei la întâmplare. Reproducerea unor astfel de erori este de obicei dificilă.

Puteți rezolva problema utilizând J2SE 5.0 . Noua semantică a cuvintelor cheie volatileface posibilă gestionarea corectă a scrierii într-o variabilă în acest caz. Acest model nou este descris în [1] :

// Funcționează cu o nouă semantică volatilă // Nu funcționează în Java 1.4 și anterioare din cauza clasei de semantică volatilă Foo { private volatile Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) helper = nou Helper (); } } return helper ; } // și alți membri ai clasei... }

Au fost propuse multe opțiuni de blocare cu verificare dublă care nu indică în mod explicit (prin volatile sau sincronizare) că un obiect este complet construit și toate sunt incorecte pentru Symantec JIT și Oracle JRE vechi [2] [3] .

Exemplu de utilizare în C#

clasă publică sigilată Singleton { private Singleton () { // inițializați o nouă instanță de obiect } private static volatil Singleton singletonInstance ; private static readonly Object syncRoot = obiect nou (); public static Singleton GetInstance () { // obiectul a fost creat dacă ( singletonInstance == null ) { // nu, nu a fost creat // doar un fir îl poate crea blocat ( syncRoot ) { // verifica dacă un alt fir a creat obiect if ( singletonInstance == null ) { // nu, nu l-am creat - create singletonInstance = new Singleton (); } } } return singletonInstance ; } }

Microsoft confirmă [4] că atunci când utilizați cuvântul cheie volatil, este sigur să utilizați modelul de blocare dublu verificat.

Un exemplu de utilizare în Python

Următorul cod Python arată un exemplu de implementare a inițializării leneșe în combinație cu modelul de blocare dublu verificat:

# necesită codare Python2 sau Python3 #-*-: UTF-8 *-* import threading clasa SimpleLazyProxy : '''inițializarea obiectului leneș fir de siguranta''' def __init__ ( self , factory ): self . __lock = threading . RLock () self . __obj = None self . __factory = fabrică def __call__ ( self ): '''funcție de acces la obiectul real dacă obiectul nu este creat, atunci va fi creat''' # încercați să obțineți acces „rapid” la obiect: obj = self . __obj dacă obj nu este None : # a reușit! return obj else : # este posibil ca obiectul să nu fi fost încă creat cu sine . __lock : # obțineți acces la obiect în modul exclusiv: obj = self . __obj dacă obj nu este None : # se dovedește că obiectul a fost deja creat. # nu-l recrea return obj else : # obiectul nu a fost încă creat cu adevărat. # hai să-l creăm! obj = self . __fabrica () sine . __obj = obj return obj __getattr__ = lambda self , name : \ getattr ( self (), name ) def lazy ( proxy_cls = SimpleLazyProxy ): '''decorator care transformă o clasă într-o clasă cu inițializare leneșă prin intermediul clasei Proxy''' class ClassDecorator : def __init__ ( self , cls ): # inițializarea decoratorului, # dar nu clasa care este decorată și nu clasa proxy sine . cls = cls def __call__ ( self , * args , ** kwargs ): # apel pentru inițializarea clasei proxy # treceți parametrii necesari clasei Proxy # pentru a inițializa clasa care este decorată returnează proxy_cls ( lambda : self . cls ( * args , ** kwargs )) returnează ClassDecorator # verificare simplă: def test_0 (): print ( ' \t\t\t *** Test start ***' ) timpul de import @lazy () # de instanțe ale acestei clase vor fi clasa inițializată lazy TestType : def __init__ ( self , name ): print ( ' %s : Created...' % name ) # crește artificial timpul de creare a obiectelor # pentru a crește competiția firelor timp . somn ( 3 ) sine . nume = nume print ( ' %s : Creat!' % name ) def test ( self ): print ( ' %s : Testing ' % self . name ) # o astfel de instanță va interacționa cu mai multe fire test_obj = TestType ( "Obiect de testare între fire" ) target_event = threading . Event () def threads_target (): # funcție pe care firele de execuție o vor executa: # așteptați un eveniment special target_event . așteaptă () # de îndată ce apare acest eveniment - # toate cele 10 fire vor accesa simultan obiectul de testare # și în acest moment este inițializat într-unul dintre firele test_obj . test () # creați aceste 10 fire de execuție cu algoritmul de mai sus threads_target() threads = [] pentru fire în interval ( 10 ): thread = threading . Thread ( target = thread_target ) fir . începe () fire . anexează ( fir ) print ( „Nu au existat accesări la obiect până acum” ) # așteaptă puțin... timp . somn ( 3 ) # ...și rulați test_obj.test() simultan pe toate firele de execuție print ( 'Declanșează evenimentul pentru a utiliza obiectul de testare!' ) target_event . set () # capăt pentru fir în fire : fir . alăturați-vă () print ( ' \t\t\t *** Sfârșitul testului ***' )

Link -uri

Note

  1. David Bacon, Joshua Bloch și alții. Declarația „Încuierea dublu verificată este spartă” . Site-ul web al lui Bill Pugh. Arhivat din original la 1 martie 2012.