2017-06-04 1 views
1

die folgende Klasse Betrachten:Wann würden Getter und Setter mit Mutex threadsicher sein?

class testThreads 
{ 
private: 
    int var; // variable to be modified 
    std::mutex mtx; // mutex 
public: 
    void set_var(int arg) // setter 
    { 
     std::lock_guard<std::mutex> lk(mtx); 
     var = arg; 
    } 

    int get_var() // getter 
    { 
     std::lock_guard<std::mutex> lk(mtx); 
     return var; 
    } 

    void hundred_adder() 
    { 
     for(int i = 0; i < 100; i++) 
     { 
      int got = get_var(); 
      set_var(got + 1); 
      sleep(0.1); 
     } 
    } 
}; 

Wenn ich zwei Threads in main() erzeugen, die jeweils mit einer Gewinde-Funktion von hundred_adder die gleiche Variable var Modifizieren, ist das Endergebnis des var immer anders dh nicht mehr als 200, aber eine andere Nummer.

Aus konzeptionellen Gründen, warum ist diese Verwendung von Mutex mit Getter-und Setter-Funktionen nicht Thread-sicher? Vereiteln die Lock-Guards nicht den Race-Condition zu var? Und was wäre eine alternative Lösung?

+3

A - erhalten, B - bekommen, A - setzen, B - setzen. :( –

Antwort

4
Thread a: get 0 
Thread b: get 0 
Thread a: set 1 
Thread b: set 1 

Und siehe da, ist var 1, obwohl es 2.

waren Es sollte offensichtlich sein sollte, dass Sie den gesamten Betrieb sperren müssen:

for(int i = 0; i < 100; i++){ 
    std::lock_guard<std::mutex> lk(mtx); 
    var += 1; 
} 

Alternativ Sie könnte die Variable atomare machen (sogar eine entspannte könnte in Ihrem Fall tun).

2

Der Code wird in thread-safe in einem Sinne angezeigt, dass er nie Teilwert der Variablen festlegen oder erhalten wird.

Aber Ihre Verwendung der Methoden garantiert nicht, dass Wert korrekt geändert wird: Lesen und Schreiben von mehreren Threads können miteinander kollidieren. Beide Threads lesen den Wert (11), beide inkrementieren ihn (auf 12) und beide setzen auf denselben (12) - jetzt haben Sie 2 gezählt, aber nur einmal effektiv erhöht.

Option zu beheben:

  • „sichere Schritt“ -Betrieb
  • bieten äquivalent InterlockedCompareExchange bieten sicheren Wert, damit Sie entsprechen die ursprünglichen und wiederholt wie nötig
  • Wrap Aufruf Code in separaten aktualisieren Mutex oder verwenden Sie einen anderen Synchronisationsmechanismus, um zu verhindern, dass sich Operationen vermischen.
3
int got = get_var(); 
    set_var(got + 1); 

Ihre get_var() und set_var() selbst sind Thread-sicher. Aber diese kombinierte Sequenz von get_var() gefolgt von set_var() ist nicht. Es gibt keinen Mutex, der diese gesamte Sequenz schützt.

Sie haben mehrere gleichzeitige Threads, die dies ausführen. Sie haben mehrere Threads, die get_var() aufrufen. Wenn der erste beendet ist und den Mutex entsperrt, kann ein anderer Thread den Mutex sofort sperren und den gleichen Wert für erhalten, den der erste Thread gemacht hat. Es gibt absolut nichts, das verhindert, dass mehrere Threads gleichzeitig sperren und dieselbe got erhalten.

Dann rufen beide Threads set_var() auf und aktualisieren den Mutex-geschützten int auf denselben Wert.

Das ist nur eine Möglichkeit, die hier passieren kann.Sie könnten leicht mehrere Threads haben, die den Mutex sequentiell erfassen und somit var um mehrere Werte inkrementieren, nur gefolgt von einem anderen, festgefahrenen Thread, der vor einigen Sekunden get_var() aufgerufen hat und jetzt nur set_var() aufruft und somit var auf 0 setzt viel kleinerer Wert.

+1

... Deshalb ist Thread-Sicherheit nicht _composable_. Dh, ein Programm/eine Bibliothek/Klasse/was auch immer völlig aus thread-sicheren Teilen zu bauen macht das Ganze nicht Thread-sicher. –

0

Warum verwenden Sie nicht einfach std :: atomic für die geteilten Daten (var in diesem Fall)? Das wird sicherer sein.

+0

... vielleicht, aber dann würde er nie die Antwort auf seine Frage wissen. –

0

Dies ist ein absoluter Klassiker. Ein Thread erhält den Wert var, gibt mutex frei und ein anderer erhält den gleichen Wert, bevor der erste Thread die Chance hat, ihn zu aktualisieren.

Folglich riskiert der Prozess Inkremente zu verlieren.

Es gibt drei offensichtliche Lösungen:

void testThreads::inc_var(){ 
    std::lock_guard<std::mutex> lk(mtx); 
    ++var; 
} 

, die sicher ist, weil der Mutex gehalten wird, bis die Variable aktualisiert wird.

Next up:

bool testThreads::compare_and_inc_var(int val){ 
    std::lock_guard<std::mutex> lk(mtx); 
    if(var!=val) return false; 
    ++var; 
    return true; 
} 

Then write code like: 

    int val; 
    do{ 
     val=get_var(); 
    }while(!compare_and_inc_var(val)); 

Das funktioniert, weil die Schleife wiederholt sich, bis sie es Aktualisieren des Wertes bestätigt es lesen. Dies könnte jedoch zu einer Live-Sperre führen, in diesem Fall muss es vorübergehend sein, da ein Thread nur einen Fortschritt machen kann, weil ein anderer es tut.

ersetzen Schließlich int var mit std::atomic<int> var und entweder verwenden ++var oder var.compare_exchange(val,val+1) oder var.fetch_add(1); es zu aktualisieren. NB: Beachten Sie compare_exchange(var,var+1) ungültig ...

++ garantiert auf std::atomic<> Arten Atom sein, aber trotz ‚suchen‘ wie ein einziger Betrieb in der Regel keine solche Garantie für int existiert.

std::atomic<> bietet auch geeignete Speicherbarrieren (und Möglichkeiten, um anzuzeigen, welche Art von Barriere erforderlich ist), um eine ordnungsgemäße Kommunikation zwischen den Threads sicherzustellen.

std::atomic<> sollte eine wartefreie, lock-freie Implementierung sein, wo verfügbar. Überprüfen Sie Ihre Dokumentation und die Flagge is_lock_free().