Kétszeres ellenőrzés blokkolás

Az oldal jelenlegi verzióját még nem ellenőrizték tapasztalt közreműködők, és jelentősen eltérhet a 2017. szeptember 20-án áttekintett verziótól ; az ellenőrzések 7 szerkesztést igényelnek .
Kétszeres ellenőrzés blokkolás
Duplán ellenőrzött zár
Leírása: Tervezési minták Nem

A duplán ellenőrzött zár egy párhuzamos tervezési minta , amelyet a  zár megszerzésével járó többletköltség csökkentésére terveztek . Először a blokkolási feltételt minden szinkronizálás nélkül ellenőrzik; a szál csak akkor próbálja megszerezni a zárat, ha az ellenőrzés eredménye azt jelzi, hogy meg kell szereznie a zárat.

Egyes nyelveken és/vagy egyes gépeken nem lehetséges ezt a mintát biztonságosan megvalósítani. Ezért néha anti-mintának is nevezik . Az ilyen tulajdonságok a Java memóriamodellben és a C++ memóriamodellben a szigorú " előtte történik " viszonyhoz vezettek .

Általában arra használják, hogy csökkentsék a többszálú programokban végzett lusta inicializálás költségeit , például a Singleton tervezési minta részeként . Egy változó lusta inicializálása esetén az inicializálás addig késik, amíg a változó értékére nincs szükség a számításban.

Java használati példa

Tekintsük a következő Java -kódot , amelyet az [1] -ből vettünk :

// Egyszálas verzió osztály Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) helper = new Helper (); visszatérő segítő ; } // és az osztály többi tagja... }

Ez a kód nem fog megfelelően működni egy többszálú programban. A metódusnak getHelper()zárolást kell szereznie abban az esetben, ha két szálból egyszerre hívják. Valójában, ha a mező helpermég nincs inicializálva, és két szál egyszerre hívja a metódust getHelper(), akkor mindkét szál megpróbál létrehozni egy objektumot, ami egy extra objektum létrehozásához vezet. Ezt a problémát szinkronizálással oldja meg, amint az a következő példában látható.

// Helyes, de "drága" többszálas verzió class Foo { private Helper helper = null ; public synchronized Helper getHelper () { if ( helper == null ) helper = new Helper (); visszatérő segítő ; } // és az osztály többi tagja... }

Ez a kód működik, de további szinkronizálási költségeket vezet be. Az első hívás getHelper()létrehozza az objektumot, és csak az getHelper()objektum inicializálása során meghívott néhány szálat kell szinkronizálni. Az inicializálás után a hívási szinkronizálás getHelper()redundáns, mivel csak a változót olvassa be. Mivel a szinkronizálás akár 100-szorosára is csökkentheti a teljesítményt, szükségtelennek tűnik a zárolás többletköltsége minden alkalommal, amikor ezt a módszert hívják: az inicializálás befejezése után a zárra már nincs szükség. Sok programozó megpróbálta optimalizálni ezt a kódot a következőképpen:

  1. Először is ellenőrzi, hogy a változó inicializálva van-e (zárolás nélkül). Ha inicializálva van, akkor azonnal visszaadja az értéket.
  2. Zár beszerzése.
  3. Ismét ellenőrzi, hogy a változó inicializálva van-e, mivel nagyon valószínű, hogy az első ellenőrzés után egy másik szál inicializálta a változót. Ha inicializálva van, akkor az értéke visszaadásra kerül.
  4. Ellenkező esetben a változó inicializálódik és visszaadásra kerül.
// Helytelen (a Symantec JIT és Java 1.4-es és korábbi verzióiban) többszálas verzió // "Double-Checked Locking" mintaosztály Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) { helper = new Helper (); } } } return helper ; } // és az osztály többi tagja... }

Intuitív szinten ez a kód helyesnek tűnik. Van azonban néhány probléma (a Java 1.4-es és korábbi verzióiban, valamint a nem szabványos JRE-megvalósításokban), amelyeket el kell kerülni. Képzelje el, hogy egy többszálú program eseményei a következőképpen zajlanak:

  1. Az A szál észreveszi, hogy a változó nincs inicializálva, majd megkapja a zárolást és megkezdi az inicializálást.
  2. Néhány programozási nyelv szemantikája[ mi? ] olyan, hogy az A szál egy megosztott változóhoz hivatkozást rendelhet egy inicializálás alatt álló objektumhoz (ami általában elég egyértelműen sérti az ok-okozati összefüggést, mert a programozó elég egyértelműen kérte, hogy adjon hozzá hivatkozást egy objektum a változóhoz [azaz hivatkozás közzététele megosztott formában] - az inicializálás utáni pillanatban , és nem az inicializálás előtti pillanatban ).
  3. A B szál észreveszi, hogy a változó inicializálva van (legalábbis ezt gondolja), és zárolás nélkül adja vissza a változó értékét. Ha a B szál most már az A szál inicializálása előtt használja a változót , a program viselkedése helytelen lesz.

A kétszeresen ellenőrzött zárolás egyik veszélye a J2SE 1.4 -ben (és korábbi verziókban) az, hogy a program gyakran megfelelően működik. Először is, a vizsgált helyzet nem fordul elő túl gyakran; másodszor, nehéz megkülönböztetni ennek a mintának a helyes megvalósítását a leírt problémával rendelkezőtől. A fordítótól , az ütemező által a szálakhoz rendelt processzoridő-kiosztástól és az egyidejűleg futó egyéb folyamatok természetétől függően a kétszeresen ellenőrzött zárolás helytelen megvalósítása által okozott hibák általában véletlenül fordulnak elő. Az ilyen hibák reprodukálása általában nehéz.

A problémát a J2SE 5.0 használatával oldhatja meg . Az új kulcsszószemantika volatileebben az esetben lehetővé teszi a változókba való írás helyes kezelését. Ezt az új mintát az [1] írja le :

// Új illékony szemantikával működik // Java 1.4-es és korábbi verzióiban nem működik a Foo volatile szemantikai osztály miatt { private volatile Helper helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) helper = new Helper (); } } return helper ; } // és az osztály többi tagja... }

Számos kétszeresen ellenőrzött zárolási opciót javasoltak, amelyek nem jelzik kifejezetten (volatile vagy szinkronizálás révén), hogy egy objektum teljesen felépített, és mindegyik helytelen a Symantec JIT és a régi Oracle JRE-k számára [2] [3] .

Használati példa C# -ban

nyilvános lezárt osztály Singleton { private Singleton () { // inicializál egy új objektumpéldányt } privát statikus volatilis Singleton singletonPéldány ; private static readonly Object syncRoot = new Object (); public static Singleton GetInstance () { // létrejött az objektum if ( singletonInstance == null ) { // nem, nincs létrehozva // csak egy szál hozhatja létre lock ( syncRoot ) { // ellenőrizze, hogy egy másik szál hozta-e létre a objektum if ( singletonInstance == null ) { // nem, nem hozta létre - létrehozás singletonInstance = new Singleton (); } } } return singletonInstance ; } }

A Microsoft megerősíti [4] , hogy a volatile kulcsszó használatakor biztonságos a Double Checked zárolási minta használata.

Példa a Python használatára

A következő Python -kód példát mutat a lusta inicializálásra a duplán ellenőrzött zárolási mintával kombinálva:

# Python2 vagy Python3 szükséges #-*- kódolás: UTF-8 *-* befűzés _ osztály SimpleLazyProxy : '''lusta objektum inicializálás cérnabiztos'''' def __init__ ( saját , gyári ): self . __lock = szálfűzés . RLock () self . __obj = Nincs saját . __gyár = gyár def __call__ ( self ): '''függvény a valós objektum eléréséhez ha az objektum nem jön létre, akkor létrejön''' # próbáljon meg "gyorsan" hozzáférni az objektumhoz: obj = self . __obj ha az obj nem Nincs : # sikerült! return obj else : # lehet, hogy az objektum még nem jött létre önmagával . _ __lock : # hozzáférést kap az objektumhoz exkluzív módban: obj = self . __obj ha az obj nem None : # kiderül, hogy az objektumot már létrehozták. # ne hozd újra return obj else : # az objektum még nem igazán jött létre. #alkossuk meg! obj = én . __gyár () önmaga . __obj = obj visszatérő obj __getattr__ = lambda self , név : \ getattr ( self (), név ) def lazy ( proxy_cls = SimpleLazyProxy ): '''dekorátor, amely egy osztályt lusta inicializálású osztállyá alakít a Proxy osztály segítségével''' class ClassDecorator : def __init__ ( self , cls ): # a dekorátor inicializálása, # de nem a díszített osztály és nem a proxy osztály önmaga . cls = cls def __call__ ( self , * args , ** kwargs ): # proxy osztály inicializálásának hívása # adja át a szükséges paramétereket a proxy osztálynak # a díszített osztály inicializálásához return proxy_cls ( lambda : self . cls ( * args , ** kwargs )) vissza ClassDecorator # egyszerű ellenőrzés: def teszt_0 (): print ( ' \t\t\t *** Teszt kezdete ***' ) behozatali idő @lazy () ennek az osztálynak # példányai lusta inicializált osztályok lesznek. TestType : def __init__ ( self , name ): print ( ' %s : Létrehozva...' % name ) # mesterségesen növelje az objektum létrehozási idejét # a szálverseny fokozása érdekében idő . aludni ( 3 ) önmaga . név = név print ( ' %s : Létrehozva !' % name ) def test ( self ): print ( ' %s : Tesztelés' % self . name ) # egy ilyen példány több szállal fog kölcsönhatásba lépni test_obj = TestType ( 'Inter-thread test object' ) target_event = szálfűzés . Event () def threads_target (): # függvény, amelyet a szálak végrehajtanak: # várjon egy különleges eseményre target_event . várj () # amint ez az esemény bekövetkezik - # mind a 10 szál egyszerre fog hozzáférni a # tesztobjektumhoz, és ebben a pillanatban az egyik test_obj szálban inicializálódik . teszt () # hozza létre ezt a 10 szálat a fenti algoritmussal threads_target() threads = [ ] szálhoz a ( 10 ) tartományban : thread = threading . Szál ( cél = thread_target ) szál . start () szálak . hozzáfűzés ( szál ) print ( 'Eddig nem volt hozzáférés az objektumhoz' ) # várj egy kicsit... idő . aludni ( 3 ) # ...és futtassa egyszerre a test_obj.test() parancsot az összes szálon print ( 'Tűzzel ki az eseményt a tesztobjektum használatához!' ) target_event . készlet () # szál vége a szálakban : szál . csatlakozz () print ( ' \t\t\t *** Teszt vége ***' )

Linkek

Jegyzetek

  1. David Bacon, Joshua Bloch és mások. A „Kétszeresen ellenőrzött zár megszakadt” nyilatkozat . Bill Pugh honlapja. Az eredetiből archiválva : 2012. március 1.