Dvojitá kontrola blokování

Aktuální verze stránky ještě nebyla zkontrolována zkušenými přispěvateli a může se výrazně lišit od verze recenzované 20. září 2017; kontroly vyžadují 7 úprav .
Dvojitá kontrola blokování
Dvojitě kontrolované zamykání
Popsáno v Návrhové vzory Ne

Dvojité zamykání je paralelní  návrhový vzor navržený tak , aby snížil režii spojenou se získáním zámku. Nejprve je zkontrolována podmínka blokování bez jakékoli synchronizace; vlákno se pokusí získat zámek pouze v případě, že výsledek kontroly naznačuje, že potřebuje získat zámek.

V některých jazycích a/nebo na některých počítačích není možné tento vzor bezpečně implementovat. Proto se mu někdy říká anti-vzor . Tyto vlastnosti vedly k přísnému vztahu „ se stane před “ v Java Memory Model a C++ Memory Model.

Běžně se používá ke snížení režie implementace pomalé inicializace ve vícevláknových programech, jako je součást návrhového vzoru Singleton . Při líné inicializaci proměnné je inicializace zpožděna, dokud není hodnota proměnné potřebná ve výpočtu.

Příklad použití Java

Zvažte následující kód Java převzatý z [1] :

// Jednovláknová verze class Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) helper = new Helper (); návrat pomocník ; } // a další členové třídy... }

Tento kód nebude správně fungovat ve vícevláknovém programu. Metoda getHelper()musí získat zámek v případě, že je volána současně ze dvou vláken. Pokud pole helperještě nebylo inicializováno a dvě vlákna volají metodu současně getHelper(), pak se obě vlákna pokusí vytvořit objekt, což povede k vytvoření dalšího objektu. Tento problém je vyřešen pomocí synchronizace, jak ukazuje následující příklad.

// Správná, ale "drahá" vícevláknová verze class Foo { private Helper helper = null ; public synchronized Helper getHelper () { if ( helper == null ) helper = new Helper (); návrat pomocník ; } // a další členové třídy... }

Tento kód funguje, ale přináší další režii synchronizace. První volání getHelper()vytvoří objekt a je getHelper()třeba synchronizovat pouze několik vláken, která budou volána během inicializace objektu. Po inicializaci je synchronizace při volání getHelper()redundantní, protože bude číst pouze proměnnou. Protože synchronizace může snížit výkon o faktor 100 nebo více, režie zamykání při každém volání této metody se zdá zbytečná: jakmile je inicializace dokončena, zámek již není potřeba. Mnoho programátorů se pokusilo tento kód optimalizovat takto:

  1. Nejprve zkontroluje, zda je proměnná inicializována (bez získání zámku). Pokud je inicializován, jeho hodnota je okamžitě vrácena.
  2. Získání zámku.
  3. Znovu zkontroluje, zda je proměnná inicializována, protože je docela možné, že po první kontrole proměnnou inicializovalo jiné vlákno. Pokud je inicializován, je vrácena jeho hodnota.
  4. V opačném případě je proměnná inicializována a vrácena.
// Nesprávná (v Symantec JIT a Java verze 1.4 a starší) vícevláknová verze // Třída vzoru "Double-Checked Locking" Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) { helper = new Helper (); } } } return helper ; } // a další členové třídy... }

Na intuitivní úrovni se tento kód zdá správný. Existují však některé problémy (v Javě 1.4 a dřívějších a nestandardních implementacích JRE), kterým je třeba se vyhnout. Představte si, že události ve vícevláknovém programu probíhají takto:

  1. Vlákno A si všimne, že proměnná není inicializována, pak získá zámek a zahájí inicializaci.
  2. Sémantika některých programovacích jazyků[ co? ] je taková, že vláknu A je povoleno přiřadit odkaz na objekt, který je v procesu inicializace, do sdílené proměnné (což obecně zcela jasně porušuje kauzální vztah, protože programátor zcela jasně požádal o přiřazení odkazu na objekt do proměnné [tj. publikovat odkaz ve sdíleném] - v okamžiku po inicializaci, nikoli v okamžiku před inicializací).
  3. Vlákno B si všimne, že proměnná je inicializována (alespoň si to myslí) a vrátí hodnotu proměnné bez získání zámku. Pokud vlákno B nyní používá proměnnou před dokončením inicializace vlákna A , chování programu bude nesprávné.

Jedním z nebezpečí používání dvojitě zkontrolovaného zamykání v J2SE 1.4 (a dřívějších) je to, že program často vypadá, že funguje správně. Za prvé, uvažovaná situace nebude nastávat příliš často; za druhé, je obtížné odlišit správnou implementaci tohoto vzoru od toho, který má popsaný problém. V závislosti na kompilátoru , alokaci procesorového času plánovačem vláknům a povaze ostatních běžících souběžných procesů se chyby způsobené nesprávnou implementací dvojitě kontrolovaného zamykání obvykle vyskytují náhodně. Reprodukce takových chyb je obvykle obtížná.

Problém můžete vyřešit pomocí J2SE 5.0 . Nová sémantika klíčových slov volatileumožňuje v tomto případě správně zvládnout zápis do proměnné. Tento nový vzor je popsán v [1] :

// Funguje s novou nestálou sémantikou // Nefunguje v Javě 1.4 a dřívějších kvůli nestálé sémantice class Foo { private volatile Helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) helper = new Helper (); } } return helper ; } // a další členové třídy... }

Bylo navrženo mnoho dvojitě zkontrolovaných možností zamykání, které explicitně (prostřednictvím volatilních nebo synchronizačních) neindikují, že objekt je plně zkonstruován, a všechny jsou nesprávné pro Symantec JIT a starší Oracle JRE [2] [3] .

Příklad použití v C#

public sealed class Singleton { private Singleton () { // inicializace nové instance objektu } soukromý statický volatilní Singleton singletonInstance ; private static pouze pro čtení Object syncRoot = new Object (); public static Singleton GetInstance () { // byl objekt vytvořen if ( singletonInstance == null ) { // ne, nevytvořeno // pouze jedno vlákno jej může vytvořit lock ( syncRoot ) { // zkontrolujte, zda jiné vlákno vytvořilo object if ( singletonInstance == null ) { // ne, nevytvořil to - create singletonInstance = new Singleton (); } } } return singletonInstance ; } }

Microsoft potvrzuje [4] , že při použití klíčového slova volatile je bezpečné použít zamykací vzor Double check.

Příklad použití v Pythonu

Následující kód Pythonu ukazuje příklad implementace líné inicializace v kombinaci s dvojitým zamykacím vzorem:

# vyžadovat Python2 nebo Python3 #-*- kódování: UTF-8 *-* importování závitů class SimpleLazyProxy : '''Inicializace líného objektu bezpečný pro vlákna''' def __init__ ( self , továrna ): self . __lock = navlékání . RLock () self . __obj = Žádné vlastní . __továrna = továrna def __call__ ( self ): '''funkce pro přístup ke skutečnému objektu pokud objekt není vytvořen, bude vytvořen''' # pokuste se získat "rychlý" přístup k objektu: obj = self . __obj pokud obj není None : # úspěšné! return obj else : # objekt možná ještě nebyl vytvořen se sebou samým . __lock : # získat přístup k objektu ve výhradním režimu: obj = self . __obj pokud obj není None : # se ukáže, že objekt již byl vytvořen. #nevytvářejte to znovu return obj else : # objekt ve skutečnosti ještě nebyl vytvořen. # pojďme to vytvořit! obj = . __factory () sebe . __obj = obj vrátit obj __getattr__ = lambda self , jméno : \ getattr ( self (), jméno ) def lazy ( proxy_cls = SimpleLazyProxy ): '''dekorátor, který změní třídu na třídu s línou inicializací pomocí třídy Proxy''' class ClassDecorator : def __init__ ( self , cls ): # inicializace dekorátoru, # ale ne zdobené třídy a ne třídy proxy sebe . cls = cls def __call__ ( self , * args , ** kwargs ): # volání pro inicializaci třídy proxy # předat potřebné parametry třídě proxy # k inicializaci zdobené třídy return proxy_cls ( lambda : self . cls ( * args , ** kwargs )) vrátit ClassDecorator #jednoduchá kontrola: def test_0 (): print ( ' \t\t\t *** Spuštění testu ***' ) čas importu @lazy () # instance této třídy budou líně inicializované třídy TestType : def __init__ ( self , jméno ): print ( ' %s : Vytvořeno...' % jméno ) # uměle prodloužit dobu vytváření objektu # pro zvýšení konkurence vláken čas . spát ( 3 ) sebe . jméno = jméno print ( ' %s : Vytvořeno!' % jméno ) def test ( self ): tisk ( ' %s : Testování' % self . jméno ) # jedna taková instance bude interagovat s více vlákny test_obj = TestType ( 'Testovací objekt mezi vlákny' ) target_event = podprocesy . Event () def threads_target (): # funkce, kterou vlákna vykonají: # čekat na speciální událost target_event . počkat () # jakmile nastane tato událost - # všech 10 vláken současně přistoupí k testovacímu objektu # a v tuto chvíli je inicializován v jednom z vláken test_obj . test () # vytvořte těchto 10 vláken pomocí výše uvedeného algoritmu threads_target() threads = [] pro vlákno v rozsahu ( 10 ): thread = threading . vlákno ( target = thread_target ) vlákno . spustit () vlákna . připojit ( vlákno ) tisknout ( 'Dosud nebyly žádné přístupy k objektu' ) # počkej chvíli... čas . spát ( 3 ) # ...a spusťte test_obj.test() současně na všech vláknech print ( 'Spusťte událost pro použití testovacího objektu!' ) target_event . nastavit () # konec pro vlákno ve vláknech : vlákno . připojit se () tisknout ( ' \t\t\t *** Konec testu ***' )

Odkazy

Poznámky

  1. David Bacon, Joshua Bloch a další. Prohlášení "Double-Checked Locking is Broken" . Web Billa Pugha. Archivováno z originálu 1. března 2012.