Duplán ellenőrzött zárolás minta

A Wikipédiából, a szabad enciklopédiából

A duplán ellenőrzött zárolás egy konkurens programtervezési minta,[1] amit arra használnak, hogy csökkentse a lockolás költségét egy előzetes lekérdezéssel, hogy fennáll-e a lockolás oka, és csak akkor lockol, ha tényleg szükséges.

A minta nem minden környezetben (hardver, nyelv, fordító) hasznos. Fennáll annak a veszélye, hogy a konkurens programozásra nem kellően felkészített fordító kioptimalizálja a második lekérdezést. Egyes esetekben antimintának tekinthető.[2]

Tipikusan lusta inicializációhoz vagy egykéhez használják. A lusta inicializáció lényege, hogy az első használat idejére halasztja el a létrehozást.

Használata C++11-ben[szerkesztés]

C++11-ben nincs szükség az egykéhez duplán ellenőrzött zárolásra: Ha a vezérlés egy olyan deklarációba lép bele, ahol éppen konkurensen inicializálnak egy változót, akkor a szálnak várnia kell, amíg a létrehozás befejeződik.[3] Tehát az egyke létrehozható úgy, mint:

Singleton& instance()
{
     static Singleton s;
     return s;
}

Ha mégis használni kell a mintát, mivel például a Visual Studio 2013 nem tartalmazza a konkurrens programozáshoz használható nyelvi szabvány "Magic statics" nevű elemét,[4] akkor megszerző-elengedő kerítést kell használni:[5]

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

Használata Javában[szerkesztés]

Tekintsük a következő kódrészletet:[2]

// Single-threaded version
class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

Konkurens környezetben ez nem működik, zárolni kell az objektumot; különben lehetséges, hogy két szál szimultán hívja a getHelper() metódust, amivel egyszerre próbálják létrehozni az objektumot, vagy az egyik egy még nem kész objektumot vesz használatba. A zárolás azt jelenti, hogy az metódust szinkronizálttá kell tenni:

// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

ami drága, ezért nem szeretik. Akár 100-szor annyi időbe is kerülhet egy szinkronizált metódus hívása, mint egy nem szinkronizálté.[6] Előny, hogy csak az első szál hozza létre az objektumot, és ezalatt a többi szál közül néhányat megvárakoztat, amíg az objektum el nem készül. Ezután minden hívás referenciát ad vissza az objektumra. Ekkor azonban már szükségtelen a zárolás, ezért sok programozó megpróbálta ezt elkerülni:

  • Zárolás nélkül lekérdezzük, hogy a változó inicializálva van-e. Ha már inicializálva van, akkor vissza lehet térni.
  • Különben megszerezzük a lockot, és újra megkérdezzük, inicializálva van-e. Ha igen, akkor vissza lehet térni.
  • Különben inicializáljuk a változót, és úgy térünk vissza.
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}

Ez az algoritmus hatékonynak látszó megoldást ad a problémára, azonban nem olyan jó, mint amilyennek kinéz, mivel bekövetkezhetnek ilyen sorrendben is az események:

  • Az A szál elkezdi létrehozni a példányt, de közben elveszik tőle a vezérlést.
  • Egyes nyelvekben a létrehozás alatt álló objektum kimásolódik egy megosztott változóba. Ez Javában is így van, ha a konstruktor inline; ekkor az allokálás után, de még a inicializálás előtt kikerül az objektum referenciája a megosztott változóba.
  • A B szál azt hiszi, hogy ez egy kész objektum, és visszaadja. A zárolással nem is próbálkozik, hanem úgy használja az objektumot, ahogy akarja, ami miatt a program hibásan kezd működni vagy összeomlik.

Korai Java verziókban a duplán ellenőrzött zárolás nehezen észrevehető és nehezen reprodukálható hibákat okozott, és nehéz volt megkülönböztetni a jó implementációt a hibástól. Problémák voltak a hordozhatósággal is, mert a különböző ütemezési stratégiák miatt nem mindenütt működött. Ezt a problémát a J2SE 5.0 oldotta meg a volatile kulcsszó bevezetésével. Ez azt biztosítja, hogy többszálú környezetben sem kezelik rosszabbul a volatile változót, mint egyszálú környezetben. Az új minta:[7][8]

// Works with acquire/release semantics for volatile in Java 1.5 and later
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

A result helyi változó feleslegesnek látszik, azonban sokat gyorsíthat a programon. Ugyanis, ha a helper inicializálva van, akkor a volatile mezőt csak egyszer kell kiolvasni. Ez akár 25%-os javulást is eredményezhet.[9]

Ha a helper objektum statikus, akkor alternatíva az egyke lusta inicializálása.[10] Lásd: Listing 16.6[11] a fent idézett szövegből:

// Correct lazy initialization in Java
class Foo {
    private static class HelperHolder {
       public static final Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

Ez arra hagyatkozik, hogy a beágyazott osztályok addig nem töltődnek be, amíg nincs rájuk hivatkozás.

A módosíthatatlan objektumokat nem lehet elrontani; ezért ha a helper utólagos módosítására nincs szükség, akkor volatile helyett final használható:[12]

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private 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;
          }
      }
      return tempWrapper.value;
   }
}

A tempWrapper helyi változóra korrektség miatt van szükség; a helperWrapper használata az ellenőrzéshez és a visszatérési értékhez problémás lehet a Java Memory Model által megengedett átrendezések miatt.[13] Performance of this implementation is not necessarily better than the volatile

Használata a Microsoft .NET keretrendszerben[szerkesztés]

A .NET keretrendszerekben a duplán ellenőrzött zárolás könnyen megvalósítható. Egy gyakran használt minta:

public class MySingleton {
    private static object myLock = new object();
    private static volatile MySingleton mySingleton = null; // 'volatile' is unnecessary in .NET 2.0 and later

    private MySingleton() {
    }

    public static MySingleton GetInstance() {
        if (mySingleton == null) { // 1st check
            lock (myLock) {
                if (mySingleton == null) { // 2nd (double) check
                    mySingleton = new MySingleton();
                    // A .NET 1.1 implicit volatile-nak jelöli a mySingleton objektumot,
                    // ami biztosítja a szükséges menmóriakorlátokat a konstruktorhívások
                    // és a mySingletonnak való írás között. A zárolás által létrehozott korlátozás nem elégséges,
                    // mivel az objektum a zár elengedése előtt láthatóvá válhat. A .NET 2.0-tól kezdve
                    // a zárolás elégséges, és a volatile-ra nincs szükség.
                }
            }
        }
        // A zár azért nem volt elégséges, mert nem minden szál próbálta megszerezni.
        // Az olvasásra megszerző zárra a mySingleton tesztelése és használata között van szükség.
        // Ezt a védelmet azért kapta a mySingleton, mert 'volatile' volt.
        // A .NET 2.0-tól kezdve nem kell a, 'volatile'.
        return mySingleton;
    }
}

Ebben a példában a lock hint a mySingleton objektum, ami akkor nem null, ha már elkészült és használatra készen áll. A .NET 4.0-ban bevezették a Lazy<T> osztályt, ami alapértelmezetten használja a duplán ellenőrzött zárolást (ExecutionAndPublication mód), és tárolja a konstruktor által dobott kivételt, vagy a Lazy<T>-nek átadott függvény eredményét is:[14]

public class MySingleton
{
    private static readonly Lazy<MySingleton> _mySingleton = new Lazy<MySingleton>(() => new MySingleton());

    private MySingleton() { }

    public static MySingleton Instance
    {
        get
        {
            return _mySingleton.Value;
        }
    }
}

Jegyzetek[szerkesztés]

  1. Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp. 353-363
  2. a b David Bacon et al. The "Double-Checked Locking is Broken" Declaration.
  3. 6.7 [stmt.dcl] p4
  4. [1]
  5. Double-Checked Locking is Fixed In C++11
  6. Boehm, Hans-J (2005. június 1.). „Threads cannot be implemented as a library”. SIGPLAN Not. 40 (6), 261–268. o, Kiadó: ACM Press. [2017. május 30-i dátummal az eredetiből archiválva]. DOI:10.1145/1064978.1065042. (Hozzáférés: 2017. június 17.)  
  7. http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
  8. http://www.oracle.com/technetwork/articles/javase/bloch-effective-08-qa-140880.html
  9. Joshua Bloch "Effective Java, Second Edition", p. 283-284
  10. Brian Goetz et al. Java Concurrency in Practice, 2006 pp348
  11. Java Concurrency in Practice – listings on website. (Hozzáférés: 2014. október 21.)
  12. [2] Javamemorymodel-discussion mailing list
  13. [3] Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c). (Hozzáférés: 2016. december 3.)
  14. Albahari, Joseph. Threading in C#: Using Threads, C# 4.0 in a Nutshell. O'Reilly Media (2010). ISBN 0-596-80095-9 „Lazy<T> actually implements […] double-checked locking. Double-checked locking performs an additional volatile read to avoid the cost of obtaining a lock if the object is already initialized.” 

Fordítás[szerkesztés]

Ez a szócikk részben vagy egészben a Double-checked locking című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.