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:
- Kontrollera att variabeln är initialiserad (utan att låsa). Om den är initierad, returnera den omedelbart.
- Skaffa låset.
- 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.
- 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:
- Tråd A märker att värdet inte är initialiserat, så den får låset och börjar initiera värdet.
- 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.
- 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
- Testa och testa-och-ställ idiomet för en lågnivålåsmekanism.
- Initialization-on-demand hållare idiom för en trådsäker ersättning i Java.
- ^ Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
- ^ a b David Bacon et al. Deklarationen "Dubbelkontrollerad låsning är trasig" .
- ^ "Stöd för C++11-14-17-funktioner (Modern C++)" .
- ^ Dubbelkontrollerad låsning är fixerad i C++11
- ^ 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 .
- ^ 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 .
- ^ Joshua Bloch "Effektiv Java, tredje upplagan", s. 372
- ^ "Kapitel 17. Trådar och lås" . docs.oracle.com . Hämtad 2018-07-28 .
- ^ Brian Goetz et al. Java Concurrency in Practice, 2006 s.348
- ^ Goetz, Brian; et al. "Java Concurrency i praktiken – listor på webbplatsen" . Hämtad 21 oktober 2014 .
- ^ [1] E-postlista för Javamemorymodel-diskussion
- ^ [2] Manson, Jeremy (2008-12-14). "Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c)" . Hämtad 3 december 2016 .
-
^
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
- Problem med den dubbelkollade låsmekanismen som fångas i Jeu Georges bloggar
- "Double Checked Locking" Beskrivning från Portland Pattern Repository
- "Double Checked Locking is Broken" Beskrivning från Portland Pattern Repository
- Papper " C++ and the Perils of Double-Checked Locking " (475 KB) av Scott Meyers och Andrei Alexandrescu
- Artikel " Dubbelkollad låsning: smart, men trasig " av Brian Goetz
- Artikel " Varning! Trådning i en multiprocessorvärld " av Allen Holub
- Dubbelkollad låsning och Singleton-mönstret
- Singleton mönster och trådsäkerhet
- flyktigt nyckelord i VC++ 2005
- Java Exempel och tidpunkt för dubbelkontrolllåslösningar
- "Effektivare Java med Googles Joshua Bloch" .