Dubbelkollad låsning

Inom mjukvaruteknik är dubbelkontrollerad låsning (även känd som "dubbelkontrollerad låsoptimering") ett mjukvarudesignmönster som används för att minska omkostnaderna för att skaffa ett lås genom att testa låskriteriet ("låstipset") innan låset skaffas . Låsning sker endast om kontrollen av låskriteriet indikerar att låsning krävs.

Mönstret kan, när det implementeras i vissa kombinationer av språk/maskinvara, vara osäkert. Ibland kan det betraktas som ett antimönster .

Det används vanligtvis för att reducera låsningsoverhead vid implementering av " lat initialisering " i en flertrådig miljö, särskilt som en del av Singleton-mönstret . Lata initiering undviker att initiera ett värde förrän första gången det öppnas.

Användning i C++11

För singelmönstret behövs ingen dubbelkontrollerad låsning:

Om kontrollen går in i deklarationen samtidigt medan variabeln initieras, ska den samtidiga exekveringen vänta på att initieringen slutförs.

§ 6.7 [stmt.dcl] p4
  
    
   
 Singleton  &  GetInstance  ()  {  static  Singleton  s  ;  returnera  s  ;  } 

C++11 och därefter tillhandahåller också ett inbyggt dubbelkontrollerat låsmönster i form av std::once_flag och std::call_once :

 
 


  
 
    
 
    

    
    



 
 

  
  
                    
   
 #inkludera  <mutex>  #inkludera  <valfritt>  // Sedan C++17  // Singleton.h  class  Singleton  {  public  :  static  Singleton  *  GetInstance  ();  privat  :  Singleton  ()  =  default  ;  static  std  ::  valfritt  <  Singleton  >  s_instance  ;  statisk  std  ::  once_flag  s_flag  ;  };  // Singleton.cpp  std  ::  valfritt  <  Singleton  >  Singleton  ::  s_instance  ;  std  ::  once_flag  Singleton  ::  s_flag  {};  Singleton  *  Singleton::GetInstance  ()  {  std  ::  call_once  (  Singleton  ::  s_flag  ,  []()  {  s_instance  .  emplace  (  Singleton  {});  });  returnera  &*  s_instans  ;  } 

Om man verkligen vill använda det dubbelkontrollerade formspråket istället för det trivialt fungerande exemplet ovan (till exempel eftersom Visual Studio före 2015 års utgåva inte implementerade C++11-standardens språk om samtidig initiering som citeras ovan), måste man använda förvärva och släpp staket:

 
 

  
 
    

 
    

    
    


  
     
       
     
      
         
         
       
    
  
   
 #include  <atomic>  #include  <mutex>  class  Singleton  {  public  :  static  Singleton  *  GetInstance  ();  privat  :  Singleton  ()  =  default  ;  statisk  std  ::  atomic  <  Singleton  *>  s_instance  ;  statisk  std  ::  mutex  s_mutex  ;  };  Singleton  *  Singleton::GetInstance  ()  {  Singleton  *  p  =  s_instance  .  ladda  (  std  ::  memory_order_acquire  );  if  (  p  ==  nullptr  )  {  // 1st check  std  ::  lock_guard  <  std  ::  mutex  >  lock  (  s_mutex  );  p  =  s_instans  .  ladda  (  std  ::  memory_order_relaxed  );  if  (  p  ==  nullptr  )  {  // 2nd (dubbel) check  p  =  new  Singleton  ();  s_instans  .  store  (  p  ,  std  ::  memory_order_release  );  }  }  returnera  p  ;  } 

Användning i Go

 

 

  
  






   
	 
		  0  
	
	 


  
	
	
	 
	 
 paketets  huvudimport  "  sync"  var  arrOnce  sync  .  När  var  arr  []  int  // getArr hämtar arr, initieras lätt vid första samtalet. Dubbelkontrollerad   //-låsning implementeras med sync.Once-biblioteksfunktionen. Den första   //-goroutinen som vinner loppet för att anropa Do() kommer att initiera arrayen, medan  // andra kommer att blockera tills Do() har slutförts. Efter att Do har körts   kommer endast en // enstaka atomjämförelse att krävas för att få arrayen.  func  getArr  ()  []  int  {  arrOnce  .  Gör  (  func  ()  {  arr  =  []  int  {  ,  1  ,  2  }  })  return  arr  }  func  main  ()  {  // tack vare dubbelkontrollerad låsning kommer två goroutiner som försöker fåArr()  // inte orsaka dubbel- initiering  go  getArr  ()  go  getArr  ()  } 

Användning i Java

Betrakta till exempel detta kodsegment i Java-programmeringsspråket som ges av (liksom alla andra Java-kodsegment):


  
       
       
            
               
        
         
    

    
 // Enkeltrådad  versionsklass  Foo  {  private  static  Helper  helper  ;  public  Helper  getHelper  ()  {  if  (  helper  ==  null  )  {  helper  =  new  Helper  ();  }  returnera  hjälpare  ;  }  // andra funktioner och medlemmar...  } 

Problemet är att detta inte fungerar när du använder flera trådar. Ett lås måste erhållas om två trådar anropar getHelper() samtidigt. Annars kan de båda försöka skapa objektet samtidigt, eller så kan man sluta få en referens till ett ofullständigt initierat objekt.

Låset erhålls genom dyrbar synkronisering, som visas i följande exempel.


  
      
        
            
               
        
         
    

    
 // Korrekt men möjligen dyr flertrådad  version  klass  Foo  {  privat  Hjälperhjälpare  ;  public  synchronized  Helper  getHelper  ()  {  if  (  helper  ==  null  )  {  helper  =  new  Helper  ();  }  returnera  hjälpare  ;  }  // andra funktioner och medlemmar...  } 

Det första anropet till getHelper() kommer dock att skapa objektet och endast de få trådar som försöker komma åt det under den tiden behöver synkroniseras; efter det får alla anrop bara en referens till medlemsvariabeln. Eftersom synkronisering av en metod i vissa extrema fall kan minska prestandan med en faktor 100 eller högre, verkar det onödigt att få och släppa ett lås varje gång den här metoden anropas: när initieringen har slutförts kommer det att dyka upp att anskaffa och släppa låsen. onödig. Många programmerare har försökt att optimera denna situation på följande sätt:

  1. Kontrollera att variabeln är initialiserad (utan att låsa). Om den är initierad, returnera den omedelbart.
  2. Skaffa låset.
  3. Dubbelkolla om variabeln redan har initierats: om en annan tråd skaffade låset först, kan den redan ha gjort initieringen. Om så är fallet, returnera den initierade variabeln.
  4. Annars, initiera och returnera variabeln.


  
      
       
            
              
                    
                       
                
            
        
         
    

    
 //  Trasig flertrådad version  // "Double-Checked Locking"  idiomklass  Foo  {  privat  Hjälperhjälpare  ;  public  Helper  getHelper  ()  {  if  (  helper  ==  null  )  {  synchronized  (  this  )  {  if  (  helper  ==  null  )  {  helper  =  new  Helper  ();  }  }  }  returnera  hjälpare  ;  }  // andra funktioner och medlemmar...  } 

Intuitivt är denna algoritm en effektiv lösning på problemet om körtiden har en fence primitiv (som hanterar minnessynlighet över exekveringsenheter), annars bör algoritmen undvikas. Tänk till exempel på följande händelseförlopp:

  1. Tråd A märker att värdet inte är initialiserat, så den får låset och börjar initiera värdet.
  2. På grund av semantiken i vissa programmeringsspråk tillåts koden som genereras av kompilatorn uppdatera den delade variabeln för att peka på ett delvis konstruerat objekt innan A har slutfört initieringen. Till exempel, i Java, om ett anrop till en konstruktor har infogats kan den delade variabeln omedelbart uppdateras när lagringen har allokerats men innan den infogade konstruktorn initierar objektet.
  3. Tråd B märker att den delade variabeln har initierats (eller så verkar det) och returnerar dess värde. Eftersom tråd B tror att värdet redan är initierat, förvärvar den inte låset. Om B använder objektet innan all initiering som gjorts av A ses av B (antingen för att A inte har initierat klart det eller för att några av de initierade värdena i objektet ännu inte har perkolerats till minnet B använder ( cachekoherens )) , kommer programmet sannolikt att krascha.

En av farorna med att använda dubbelkontrollerad låsning i J2SE 1.4 (och tidigare versioner) är att det ofta verkar fungera: det är inte lätt att skilja mellan en korrekt implementering av tekniken och en som har subtila problem. Beroende på kompilatorn , sammanflätningen av trådar av schemaläggaren och arten av annan samtidig systemaktivitet , kan fel som är ett resultat av en felaktig implementering av dubbelkontrollerad låsning endast inträffa intermittent. Att återskapa misslyckanden kan vara svårt.

Från och med J2SE 5.0 har detta problem åtgärdats. Det flyktiga nyckelordet ser nu till att flera trådar hanterar singleton-instansen korrekt. Detta nya formspråk beskrivs i [3] och [4] .



  
       
       
           
            
              
                  
                    
                         
                
            
        
         
    

    
 // Fungerar med förvärva/släppa semantik för volatile i Java 1.5 och senare  // Broken under Java 1.4 och tidigare semantik för volatile  class  Foo  {  private  volatile  Helper  helper  ;  public  Helper  getHelper  ()  {  Helper  localRef  =  helper  ;  if  (  localRef  ==  null  )  {  synchronized  (  this  )  {  localRef  =  helper  ;  if  (  localRef  ==  null  )  {  helper  =  localRef  =  new  Helper  ();  }  }  }  returnera  localRef  ;  }  // andra funktioner och medlemmar...  } 

Notera den lokala variabeln " localRef ", som verkar onödig. Effekten av detta är att i fall där hjälparen redan är initierad (dvs. för det mesta) nås det flyktiga fältet endast en gång (på grund av " retur lokalreferens; " istället för " returhjälparen; "), vilket kan förbättra metodens totala prestanda med så mycket som 40 procent.

Java 9 introducerade VarHandle- klassen, som tillåter användning av avslappnade atomer för att komma åt fält, vilket ger något snabbare avläsningar på maskiner med svaga minnesmodeller, till bekostnad av svårare mekanik och förlust av sekventiell konsistens (fältåtkomster deltar inte längre i synkroniseringsordningen , den globala ordningen för åtkomst till flyktiga fält).


  
       

       
           
            
              
                  
                    
                       
                    
                
            
        
         
    

        
       
          
    
        
         
    

     
         
               
                
            
              
        
    

    
 // Fungerar med förvärva/släppa semantik för VarHandles introducerad i Java 9  klass  Foo  {  privat  volatile  Helper  helper  ;  public  Helper  getHelper  ()  {  Helper  localRef  =  getHelperAcquire  ();  if  (  localRef  ==  null  )  {  synchronized  (  this  )  {  localRef  =  getHelperAcquire  ();  if  (  localRef  ==  null  )  {  localRef  =  new  Helper  ();  setHelperRelease  (  localRef  );  }  }  }  returnera  localRef  ;  }  privat  statisk  slutlig  VarHandle  HELPER  ;  privat  Hjälpare  getHelperAcquire  ()  {  return  (  Hjälpare  )  HJÄLPARE  .  getAcquire  (  detta  );  }  privat  void  setHelperRelease  (  Hjälparvärde  )  {  HJÄLPARE  .  _  setRelease  (  detta  ,  värde  );  }  static  {  prova  {  MethodHandles  .  Lookup  lookup  =  MethodHandles  .  uppslag  ();  HJÄLPARE  =  ​​uppslag  .  findVarHandle  (  Foo  .  class  ,  "helper"  ,  Helper  .  class  );  }  catch  (  ReflectiveOperationException  e  )  {  throw  new  ExceptionInInitializerError  (  e  );  }  }  // andra funktioner och medlemmar...  } 

Om hjälpobjektet är statiskt (en per klassladdare) är ett alternativ initierings-on-demand-hållarformen (se lista 16.6 från den tidigare citerade texten.)


  
        
              
    

        
         
    
 // Korrigera lat initialisering i Java  -klassen  Foo  {  private  static  class  HelperHolder  {  public  static  final  Helper  helper  =  new  Helper  ();  }  public  static  Helper  getHelper  ()  {  return  HelperHolder  .  hjälpare  ;  }  } 

Detta bygger på det faktum att kapslade klasser inte laddas förrän de refereras.

Semantik för det sista fältet i Java 5 kan användas för att säkert publicera hjälpobjektet utan att använda volatile :

   
       
       
          
    


   
     

      
         

          
            
                  
                      
              
                
          
      
       
   
 public  class  FinalWrapper  <  T  >  {  public  final  T  value  ;  public  FinalWrapper  (  T  -värde  )  {  detta  .  värde  =  värde  ;  }  }  public  class  Foo  {  privat  FinalWrapper  <  Helper  >  helperWrapper  ;  public  Helper  getHelper  ()  {  FinalWrapper  <  Helper  >  tempWrapper  =  helperWrapper  ;  if  (  tempWrapper  ==  null  )  {  synchronized  (  this  )  {  if  (  helperWrapper  ==  null  )  {  helperWrapper  =  new  FinalWrapper  <  Helper  >  (  new  Helper  ());  }  tempWrapper  =  helperWrapper  ;  }  }  returnera  tempWrapper  .  värde  ;  }  } 

Den lokala variabeln tempWrapper krävs för korrekthet: att helt enkelt använda helperWrapper för både nollkontroller och retursatsen kan misslyckas på grund av läsomordning som tillåts under Java Memory Model. Prestanda för denna implementering är inte nödvändigtvis bättre än den flyktiga implementeringen.

Användning i C#

Dubbelkontrollerad låsning kan implementeras effektivt i .NET. Ett vanligt användningsmönster är att lägga till dubbelkontrollerad låsning till Singleton-implementeringar:

  

          
         

       

       
    
            
        
             
            
                    
                
                       
                
            
        

         
    
 public  class  MySingleton  {  privat  statiskt  objekt  _myLock  =  nytt  objekt  ();  privat  statisk  MySingleton  _mySingleton  =  null  ;  private  MySingleton  ()  {  }  public  static  MySingleton  GetInstance  ()  {  if  (  _mySingleton  is  null  )  // The first check  {  lock  (  _mySingleton  )  {  if  (  _mySingleton  is  null  )  // Den andra (double) check  {  _mySingleton  =  new  MySingleton  ( );  }  }  }  returnera  _mySingleton  ;  }  } 

I det här exemplet är "låstipset" objektet _mySingleton som inte längre är null när det är färdigbyggt och klart för användning.

I .NET Framework 4.0 introducerades klassen Lazy<T>, som internt använder dubbelkontrollerad låsning som standard (ExecutionAndPublication-läge) för att lagra antingen undantaget som kastades under konstruktionen eller resultatet av funktionen som skickades till Lazy <T> :

  

              

       

         
 public  class  MySingleton  {  private  static  readonly  Lazy  <  MySingleton  >  _mySingleton  =  new  Lazy  <  MySingleton  >(()  =>  new  MySingleton  ());  private  MySingleton  ()  {  }  public  static  MySingleton  Instance  =>  _mySingleton  .  Värde  ;  } 

Se även

  1. ^ Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. ^ a b David Bacon et al. Deklarationen "Dubbelkontrollerad låsning är trasig" .
  3. ^ "Stöd för C++11-14-17-funktioner (Modern C++)" .
  4. ^ Dubbelkontrollerad låsning är fixerad i C++11
  5. ^ Boehm, Hans-J (juni 2005). "Trådar kan inte implementeras som ett bibliotek" (PDF) . ACM SIGPLAN-meddelanden . 40 (6): 261–268. doi : 10.1145/1064978.1065042 .
  6. ^ Haggar, Peter (1 maj 2002). "Dubbelkollad låsning och Singleton-mönstret" . IBM. Arkiverad från originalet 2017-10-27 . Hämtad 2022-05-19 .
  7. ^ Joshua Bloch "Effektiv Java, tredje upplagan", s. 372
  8. ^ "Kapitel 17. Trådar och lås" . docs.oracle.com . Hämtad 2018-07-28 .
  9. ^ Brian Goetz et al. Java Concurrency in Practice, 2006 s.348
  10. ^ Goetz, Brian; et al. "Java Concurrency i praktiken – listor på webbplatsen" . Hämtad 21 oktober 2014 .
  11. ^ [1] E-postlista för Javamemorymodel-diskussion
  12. ^ [2] Manson, Jeremy (2008-12-14). "Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c)" . Hämtad 3 december 2016 .
  13. ^   Albahari, Joseph (2010). "Tråda i C#: Använda trådar" . C# 4.0 i ett nötskal . O'Reilly Media. ISBN 978-0-596-80095-6 . Lazy<T> implementerar faktiskt […] dubbelkontrollerad låsning. Dubbelkontrollerad låsning utför en extra flyktig avläsning för att undvika kostnaden för att få ett lås om objektet redan är initierat.

externa länkar