Dobbelttjek blokering

Den aktuelle version af siden er endnu ikke blevet gennemgået af erfarne bidragydere og kan afvige væsentligt fra den version , der blev gennemgået den 20. september 2017; checks kræver 7 redigeringer .
Dobbelttjek blokering
Dobbelttjekket låsning
Beskrevet i Design Patterns Ikke

Dobbelt tjekket låsning er et  parallelt designmønster designet til at reducere det overhead, der er forbundet med at få en lås. Først kontrolleres blokeringstilstanden uden nogen synkronisering; tråden forsøger kun at erhverve låsen, hvis resultatet af kontrollen indikerer, at den skal erhverve låsen.

På nogle sprog og/eller på nogle maskiner er det ikke muligt at implementere dette mønster sikkert. Derfor kaldes det nogle gange et anti-mønster . Sådanne funktioner har ført til den strenge rækkefølge " sker før "-forholdet i Java Memory Model og C++ Memory Model.

Det bruges almindeligvis til at reducere omkostningerne ved at implementere doven initialisering i flertrådede programmer, såsom som en del af Singleton-designmønsteret . Med doven initialisering af en variabel forsinkes initialiseringen, indtil værdien af ​​variablen er nødvendig i beregningen.

Eksempel på Java- brug

Overvej følgende Java -kode hentet fra [1] :

// Single threaded version class Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) helper = new Helper (); returnere hjælper ; } // og andre medlemmer af klassen... }

Denne kode vil ikke fungere korrekt i et program med flere tråde. Metoden getHelper()skal anskaffe en lås, hvis den kaldes samtidigt fra to tråde. Faktisk, hvis feltet helperendnu ikke er blevet initialiseret, og to tråde kalder metoden på samme tid getHelper(), vil begge tråde forsøge at skabe et objekt, hvilket vil føre til oprettelsen af ​​et ekstra objekt. Dette problem løses ved at bruge synkronisering, som vist i følgende eksempel.

// Korrekt, men "dyr" multi-threaded version klasse Foo { private Helper helper = null ; public synchronized Helper getHelper () { if ( helper == null ) helper = new Helper (); returnere hjælper ; } // og andre medlemmer af klassen... }

Denne kode virker, men den introducerer yderligere synkroniseringsomkostninger. Det første kald getHelper()vil oprette objektet, og kun de få tråde, der vil blive kaldt getHelper()under objektinitialisering, skal synkroniseres. Når først den er initialiseret, er synkroniseringen ved opkald getHelper()redundant, da den kun vil læse variablen. Da synkronisering kan reducere ydeevnen med en faktor på 100 eller mere, virker overheaden ved låsning, hver gang denne metode kaldes, unødvendig: når først initialiseringen er fuldført, er låsen ikke længere nødvendig. Mange programmører har forsøgt at optimere denne kode på denne måde:

  1. Først tjekker den, om variablen er initialiseret (uden at få en lås). Hvis den initialiseres, returneres dens værdi med det samme.
  2. At få en lås.
  3. Den tjekker igen for at se, om variablen er initialiseret, da det er meget muligt, at en anden tråd efter den første kontrol initialiserede variablen. Hvis den initialiseres, returneres dens værdi.
  4. Ellers initialiseres variablen og returneres.
// Forkert (i Symantec JIT og Java versioner 1.4 og tidligere) multi-threaded version // "Double - Checked Locking" mønsterklasse Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) { helper = new Helper (); } } } returner hjælper ; } // og andre medlemmer af klassen... }

På et intuitivt niveau virker denne kode korrekt. Der er dog nogle problemer (i Java 1.4 og tidligere og ikke-standard JRE-implementeringer), som måske bør undgås. Forestil dig, at begivenheder i et flertrådet program forløber således:

  1. Tråd A bemærker, at variablen ikke er initialiseret, henter derefter låsen og begynder initialiseringen.
  2. Semantik af nogle programmeringssprog[ hvad? ] er sådan, at tråd A får lov til at tildele en reference til et objekt, der er i gang med at initialisere, til en delt variabel (hvilket i almindelighed helt klart overtræder årsagssammenhængen, fordi programmøren ganske tydeligt bad om at tildele en reference til et objekt til variablen [det vil sige at publicere en reference i shared] - i øjeblikket efter initialisering og ikke i øjeblikket før initialisering).
  3. Tråd B bemærker, at variablen er initialiseret (det tror den i hvert fald) og returnerer værdien af ​​variablen uden at opnå en lås. Hvis tråd B nu bruger variablen før tråd A er færdig med at initialisere, vil programmets opførsel være forkert.

En af farerne ved at bruge dobbelttjekket låsning i J2SE 1.4 (og tidligere) er, at programmet ofte ser ud til at fungere korrekt. For det første vil den betragtede situation ikke forekomme særlig ofte; for det andet er det svært at skelne den korrekte implementering af dette mønster fra den, der har det beskrevne problem. Afhængigt af compileren , planlæggerens tildeling af processortid til tråde og arten af ​​andre kørende samtidige processer, opstår fejl forårsaget af forkert implementering af dobbelttjekket låsning normalt tilfældigt. Det er normalt vanskeligt at gengive sådanne fejl.

Du kan løse problemet ved at bruge J2SE 5.0 . Det nye søgeord semantik volatilegør det muligt at håndtere skrivning til en variabel korrekt i dette tilfælde. Dette nye mønster er beskrevet i [1] :

// Fungerer med ny flygtig semantik // Virker ikke i Java 1.4 og tidligere på grund af flygtig semantikklasse Foo { private volatile Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) helper = new Helper (); } } returner hjælper ; } // og andre medlemmer af klassen... }

Mange dobbelttjekkede låsemuligheder er blevet foreslået, som ikke eksplicit (via flygtig eller synkronisering) indikerer, at et objekt er fuldt konstrueret, og alle af dem er forkerte for Symantec JIT og ældre Oracle JRE'er [2] [3] .

Eksempel på brug i C#

offentlig forseglet klasse Singleton { privat Singleton () { // initialiser en ny objektinstans } privat statisk flygtig Singleton singletonInstance ; privat statisk skrivebeskyttet Objekt syncRoot = nyt objekt (); public static Singleton GetInstance () { // er objektet blevet oprettet if ( singletonInstance == null ) { // nej, ikke oprettet // kun én tråd kan oprette den lås ( syncRoot ) { // tjek om en anden tråd har oprettet object if ( singletonInstance == null ) { // nej, oprettede det ikke - create singletonInstance = new Singleton (); } } } returner singletonInstance ; } }

Microsoft bekræfter [4] , at når du bruger det flygtige nøgleord, er det sikkert at bruge det dobbelttjekkede låsemønster.

Et eksempel på brug i Python

Den følgende Python -kode viser et eksempel på implementering af doven initialisering i kombination med det dobbelttjekkede låsemønster:

# kræver Python2 eller Python3 #-*- kodning: UTF-8 *-* importere gevind klasse SimpleLazyProxy : '''initialisering af dovent objekt trådsikker''' def __init__ ( selv , fabrik ): selv . __lås = trådning . Lås () selv . __obj = Ingen selv . __fabrik = fabrik def __call__ ( self ): '''funktion for at få adgang til det rigtige objekt hvis objektet ikke er oprettet, bliver det oprettet''' # prøv at få "hurtig" adgang til objektet: obj = self . __obj hvis obj ikke er Ingen : # lykkedes! return obj else : # objektet er muligvis ikke oprettet endnu med sig selv . __lock : # få adgang til objektet i eksklusiv tilstand: obj = self . __obj hvis obj ikke er Ingen : # viser sig, at objektet allerede er oprettet. # genskab det ikke return obj else : # objektet er ikke rigtig blevet oprettet endnu. # lad os skabe det! obj = selv . __fabrik () selv . __obj = obj returnere obj __getattr__ = lambda selv , navn : \ getattr ( selv (), navn ) def lazy ( proxy_cls = SimpleLazyProxy ): '''dekorator, der gør en klasse til en klasse med doven initialisering ved hjælp af Proxy-klassen''' klasse ClassDecorator : def __init__ ( self , cls ): # initialisering af dekoratøren, # men ikke klassen, der dekoreres og ikke proxy-klassen selv . cls = cls def __call__ ( selv , * args , ** kwargs ): # opkald til proxy klasse initialisering # videregive de nødvendige parametre til Proxy-klassen # for at initialisere klassen, der dekoreres returner proxy_cls ( lambda : self . cls ( * args , ** kwargs )) returner ClassDecorator # simpel kontrol: def test_0 (): print ( ' \t\t\t *** Teststart ***' ) import tid @lazy () # forekomster af denne klasse vil være doven-initialiseret klasse TestType : def __init__ ( selv , navn ): print ( ' %s : Oprettet...' % navn ) # kunstigt øge objektoprettelsestiden # for at øge trådkonkurrencen tid . søvn ( 3 ) selv . navn = navn print ( ' %s : Oprettet!' % navn ) def test ( self ): print ( ' %s : Testing ' % self . name ) # en sådan instans vil interagere med flere tråde test_obj = TestType ( 'Inter-thread testobjekt' ) target_event = trådning . Hændelse () def threads_target (): # funktion, som tråde vil udføre: # vent på en speciel begivenhed target_event . vent () # så snart denne hændelse opstår - # vil alle 10 tråde samtidigt få adgang til testobjektet # og i dette øjeblik initialiseres det i en af ​​trådene test_obj . test () # opret disse 10 tråde med ovenstående algoritme threads_target() tråde = [] for tråd i området ( 10 ): tråd = trådning . Tråd ( target = thread_target ) tråd . start () tråde . tilføje ( tråd ) print ( 'Der har ikke været nogen adgang til objektet indtil nu' ) #vent lidt... tid . søvn ( 3 ) # ...og kør test_obj.test() samtidigt på alle tråde print ( 'Brand hændelse for at bruge testobjekt!' ) target_event . sæt () # ende for tråd i tråde : tråd . deltage () print ( ' \t\t\t *** Slut på test ***' )

Links

Noter

  1. David Bacon, Joshua Bloch og andre. Erklæringen "Dobbeltkontrolleret låsning er brudt" . Bill Pughs hjemmeside. Arkiveret fra originalen den 1. marts 2012.