2015-05-05 7 views
6

Ein Beispiel in Meyers Buch Effektive Moderne C++, Punkt 16.Ist meine Double-Checked Locking Pattern-Implementierung richtig?

in einer Klasse-Caching ein teurer zu berechnen int, könnten Sie versuchen, ein Paar std :: Atom avriables verwenden statt ein Mutex:

class Widget { 
public: 
    int magicValue() const { 
     if (cachedValid) { 
      return cachedValue; 
     } else { 
      auto val1 = expensiveComputation1(); 
      auto val2 = expensiveComputation2(); 

      cachedValue = va1 + val2; 
      cacheValid = true; 
      return cachedValue; 
     } 
    } 
private: 
    mutable std::atomic<bool> cacheValid { false }; 
    mutable std::atomic<int> cachedValue; 
}; 

Dies funktioniert, aber manchmal wird es viel härter zu arbeiten, als es should.Consider: ein Thread Widget ruft :: magicValue, sieht cacheValid als false, führt die zwei teuren Berechnungen durch und ordnet ihre Summe CachedValud zu. An diesem Punkt, ein zweiter Thread calss Widget :: magicValue, sieht auch cacheValid als falsch, und führt somit aus den gleichen teuren Berechnungen, die der erste Thread nur abgeschlossen hat.

Dann gibt er eine Lösung mit Mutex:

class Widget { 
public: 
    int magicValue() const { 
     std::lock_guard<std::mutex> guard(m); 
     if (cacheValid) { 
      return cachedValue; 
     } else { 
      auto val1 = expensiveComputation1(); 
      auto val2 = expensiveComputation2(); 

      cachedValue = va1 + val2; 
      cacheValid = true; 
      return cachedValue; 
     } 
    } 
private: 
    mutable std::mutex m; 
    mutable bool cacheValid { false }; 
    mutable int cachedValue; 
}; 

Aber ich glaube, die Lösung nicht so effecient ist, halte ich Mutex zu kombinieren und Atom bilden ein Doppel-Karomuster Sperren als unten.

class Widget { 
public: 
    int magicValue() const { 
     if (!cacheValid) { 
      std::lock_guard<std::mutex> guard(m); 
      if (!cacheValid) { 
       auto val1 = expensiveComputation1(); 
       auto val2 = expensiveComputation2(); 

       cachedValue = va1 + val2; 
       cacheValid = true; 
      } 
     } 
     return cachedValue; 
    } 
private: 
    mutable std::mutex m; 
    mutable std::atomic<bool> cacheValid { false }; 
    mutable std::atomic<int> cachedValue; 
}; 

Weil ich ein Neuling in Multi-Thread-Programmierung bin, so möchte ich wissen:

  • Ist mein Code richtig?
  • Ist die Leistung besser?

EDIT:


den Code behoben. (! CachedValue) wenn -> if (cacheValid!)

+0

Im Prinzip ja, sind Sie hier richtig. Aber wenn wir davon ausgehen, dass die Berechnungen viel länger dauern als das Setzen der Atomics, ist die Wahrscheinlichkeit für Ihr Szenario sehr gering und die Verwendung der Atomics vermeidet teure Mutexe. –

+0

Ich denke, Ihr Ansatz ist korrekt und besser als die beiden vorherigen. Es erklärte nur das * Double-Checked Locking Pattern *. – Lingxi

+0

Ich glaube nicht, dass es korrekt ist, wenn ein zweiter Thread die cacheValid nach der ersten ausgewertet hat, aber bevor der Guard instanziiert wird. Ich denke, die Auswirkung kann gleich dem ersten Beispiel sein. – HappyCactus

Antwort

0

Recht mein Code?

Ja. Die Anwendung des Double-Checked Locking Patterns ist korrekt. Aber sehen Sie unten für einige Verbesserungen.

Ist die Leistung besser?

Wenn mit vollständig gesperrten Variante (2. in Ihrem Beitrag) im Vergleich, hat es meist eine bessere Leistung, bis magicValue() nur einmal aufgerufen wird (aber auch in diesem Fall Leistungsverluste sind vernachlässigbar klein).

Im Vergleich mit Lockless-Variante (1. in Ihrem Beitrag), zeigt Ihr Code eine bessere Leistung, bis Wert Berechnung schneller ist als warten auf Mutex.

Z. B. Summe von 10 Werten ist (in der Regel) schneller als auf Mutex warten. In diesem Fall ist die erste Variante vorzuziehen. Von der anderen Seite, 10 liest aus Datei ist langsamer als warten auf Mutex, so ist Ihre Variante besser als 1st.


Eigentlich gibt es einfache improvemnts, um Ihren Code, der es schneller zu machen (zumindest auf einigen Maschinen) und Code-Verständnis verbessern:

  1. cachedValue Variable nicht atomar erfordert semantische bei alle. Es wird durch cacheValid Flag geschützt, welche Atomizität die ganze Arbeit macht. Darüber hinaus kann ein einzelnes atomares Flag mehrere nicht atomare Werte schützen.

  2. Auch als https://stackoverflow.com/a/30049946/3440745 in dieser Antwort erwähnt, wenn der Zugriff auf cacheValid Flag Sie sequenzielle Konsistenz, um nicht brauchen (die standardmäßig angewandt, wenn Sie einfach Atom-Variable lesen oder schreiben), Release-acquire, um ausreichend ist.


class Widget { 
public: 
    int magicValue() const { 
     //'Acquire' semantic when read flag. 
     if (!cacheValid.load(std::memory_order_acquire)) { 
      std::lock_guard<std::mutex> guard(m); 
      // Reading flag under mutex locked doesn't require any memory order. 
      if (!cacheValid.load(std::memory_order_relaxed)) { 
       auto val1 = expensiveComputation1(); 
       auto val2 = expensiveComputation2(); 

       cachedValue = va1 + val2; 
       // 'Release' semantic when write flag 
       cacheValid.store(true, std::memory_order_release); 
      } 
     } 
     return cachedValue; 
    } 
private: 
    mutable std::mutex m; 
    mutable std::atomic<bool> cacheValid { false }; 
    mutable int cachedValue; // Atomic isn't needed here. 
}; 
+0

'cachedValue' muss in dieser Lösung nicht atomar sein, da das Schreiben auf ihn durch den Mutex geschützt wird. Im Code in Meyers Buch (und in der vorgeschlagenen Lösung von Maxim Egorushkin) gibt es keinen Mutex, der verhindert, dass mehrere Threads gleichzeitig "cachedValue" zuweisen (alle diese Threads haben einen 'cacheValid'-Wert von' false' bei) gesehen der Anfang der Funktion). In diesem Fall ist es wichtig, dass "cachedValue" atomar ist, da ansonsten simultane Schreiboperationen zu undefiniertem Verhalten führen. – KnowItAllWannabe

+0

@KnowItAllWannabe: 'cachedValue' ist in dieser Lösung bereits nicht atomar. Wenn du 'cacheValid' meinst, sollte es atomisch sein, weil es (erstmalig) aus dem Mutex-Schutz heraus überprüft wird. Dies ist das Hauptmerkmal von DCLP, das Flag sollte * volatile * sein (in Sprachen, in denen dieser Begriff Thread-bezogen ist). zB Java) oder * Atomic *. – Tsyvarev

+0

Mein Kommentar war genau das: ein Kommentar. Ich stimme zu, dass die angegebene Implementierung in Ordnung ist (und dies auch gesagt hat), aber in Ihrem Punkt 1 über dem Code schreiben Sie, dass die Variable "cachedValue" überhaupt keine atomare Semantik erfordert, weil sie durch das Flag "cacheValid" geschützt ist . " Das ist nicht wahr. 'cachedValue' muss nicht atomar sein, da es durch den Mutex geschützt ist.Wie ich in meinem Kommentar zu Maxim Egorushkins Antwort unten bemerkte, müssen, wenn kein Mutex vorhanden ist, sowohl "cacheValid" als auch "cachedValue" atomar sein. – KnowItAllWannabe

-1

Es ist nicht korrekt:

int magicValue() const { 
    if (!cachedValid) { 

     // this part is unprotected, what if a second thread evaluates 
     // the previous test when this first is here? it behaves 
     // exactly like in the first example. 

     std::lock_guard<std::mutex> guard(m); 
     if (!cachedValue) { 
      auto val1 = expensiveComputation1(); 
      auto val2 = expensiveComputation2(); 

      cachedValue = va1 + val2; 
      cachedValid = true; 
     } 
    } 
    return cachedValue; 
+1

Ich glaube nicht, dass es ein Problem für mehrere Threads in diesem Teil sein. – Lingxi

+0

Es ist, es sei denn, der zweite Test sollte als "CachedValid" und nicht "CachedValue" gelesen werden. Ich habe nicht vermutet, dass es sich um einen Tippfehler handelt, aber es ist offensichtlich. – HappyCactus

+0

Du hast ein Paar scharfe Augen :-) Hab den Tippfehler nicht bemerkt. – Lingxi

2

Wie HappyCactus wies darauf hin, die zweite Prüfung if (!cachedValue) tatsächlich if (!cachedValid) sein sollte. Abgesehen von diesem Tippfehler denke ich, dass Ihre Demonstration des Double-Checked Locking Patterns korrekt ist. Ich denke jedoch, dass es unnötig ist, std::atomic auf zu verwenden. Der einzige Ort, an dem cachedValue geschrieben wird, ist cachedValue = va1 + val2;. Bevor es fertig ist, wird kein Thread jemals die Anweisung return cachedValue; erreichen, die die einzige Stelle ist, an der gelesen wird. Daher ist es unmöglich, dass ein Schreiben und ein Lesen gleichzeitig erfolgt. Und es gibt kein Problem mit gleichzeitigen Lesevorgängen.

+0

Es ist notwendig, es atomar zu machen: http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html. Da das Flag atomar ist, kann sein Schreiben über Prozessoren hinweg synchronisiert sein, während der nicht-atomare Wert dies nicht ist. Sie könnten also cacheValid = true sehen, aber cacheValue =/* auf diesem Prozessor nicht initialisiert */ –

+0

@BillyONeal Nein. Die Semantik der Speicherordnung, die für 'cacheValid' verwendet wird, ist' memory_order_seq_cst'. Es richtet die erforderlichen Synchronisationsbeziehungen ein. Wenn 'cacheValid' gelesen wird" true ", muss die Änderung an" cacheValue "auch auf demselben Prozessor gesehen werden. Dies ist der Grund, warum Atomics verwendet werden können, um Nicht-Atomics zu ordnen. – Lingxi

0

Sie können Ihre Lösung etwas effizienter machen, indem Sie die Speicheranforderungen reduzieren. Die Reihenfolge der sequentiellen Konsistenzspeicher für atomare Operationen ist hier nicht erforderlich.

Der Leistungsunterschied kann auf x86 vernachlässigbar sein, ist aber auf ARM bemerkbar, da die sequenzielle Konsistenzspeicherordnung auf ARM teuer ist. Weitere Details finden Sie unter “Strong” and “weak” hardware memory models by Herb Sutter.

Vorgeschlagene Änderungen:

class Widget { 
public: 
    int magicValue() const { 
     if (cachedValid.load(std::memory_order_acquire)) { // Acquire semantics. 
      return cachedValue; 
     } else { 
      auto val1 = expensiveComputation1(); 
      auto val2 = expensiveComputation2(); 

      cachedValue = va1 + val2; // Non-atomic write. 

      // Release semantics. 
      // Prevents compiler and CPU store reordering. 
      // Makes this and preceding stores by this thread visible to other threads. 
      cachedValid.store(true, std::memory_order_release); 
      return cachedValue; 
     } 
    } 
private: 
    mutable std::atomic<bool> cacheValid { false }; 
    mutable int cachedValue; // Non-atomic. 
}; 
+0

Haben Sie die Situation in Betracht gezogen *: Ein Thread ruft Widget :: magicValue auf, sieht cacheValid als false, führt die beiden teuren Berechnungen durch und ordnet ihre Summe cachedValud zu. An diesem Punkt, ein zweiter Thread calss Widget :: magicValue, sieht auch cacheValid als false und führt somit die gleichen teuren Berechnungen aus, die der erste Thread gerade beendet hat. * – prehistoricpenguin

+0

@prehistoricpenguin Wenn die Berechnung von 'cachedValue' nur einmal durchgeführt werden muss, Sie können wahrscheinlich einen Mutex verwenden. –

+0

Downvoted, weil, wenn zwei Threads 'cacheValid' als' false' lesen, beide 'cachedValue' berechnen, und wenn beide gleichzeitig eine Zuweisung zu' cachedValue' vornehmen, haben Sie zwei Threads, die gleichzeitig in einen nicht-atomaren schreiben und das führt zu undefiniertem Verhalten. – KnowItAllWannabe