2016-09-07 6 views
14

Prüft der Compiler einfach, welche Variablen zwischen den Sperren- und Entsperrungsanweisungen geändert werden, und bindet sie an den Mutex, sodass es exklusiven Zugriff darauf gibt?Wie bindet ein C++ std :: mutex an eine Ressource?

Oder ein mutex.lock() alle Ressourcen sperren, die im aktuellen Bereich sichtbar sind?

+13

Der Mutex sperrt keine Variablen, er blockiert lediglich einen Thread, der versucht, ihn zu erhalten.Dies hat den Effekt, Variablen zu schützen, wenn das Programm nur zwischen einem 'lock()' - und einem 'unlock() '-Aufruf auf sie zugreift. Das Vorhandensein des Mutex 'lock()' und 'unlock()' im Code verhindert auch, dass der Compiler den Speicherzugriff der so geschützten * accesses * neu anordnet. –

+3

Sie müssen auch sicherstellen, dass die Threads das gleiche Mutex-Objekt verwenden. Einen unterschiedlichen Mutex in jedem Thread zu haben, wird nichts tun. Stattdessen blockiert es, wenn ein anderer Thread versucht, einen bereits gesperrten Mutex wie @RichardHodges zu sperren. – Hayt

+0

Danke, das hat es wirklich geklärt! – niko

Antwort

15

Da m ist eine Variable vom Typ std::mutex:

diese Sequenz Stellen Sie sich vor:

int a; 
m.lock(); 
b += 1; 
a = b; 
m.unlock(); 
do_something_with(a); 

Es gibt ein 'offensichtliches', was hier los ist:

Die Zuordnung eines von b und das Inkrement von b wird vor Interferenz von anderen Threads "geschützt", weil andere Threads versuchen, dasselbe zu sperren m und blockiert werden, bis wir m.unlock() aufrufen.

Und es ist eine subtile Sache los.

In Singlethread-Code wird der Compiler versuchen, Lasten und speichert neu zu ordnen. Ohne die Schlösser, würde der Compiler frei sein, um effektiv den Code neu zu schreiben, wenn dies effizienter auf dem Chipsatz entpuppt:

int a = b + 1; 
// m.lock(); 
b = a; 
// m.unlock(); 
do_something_with(a); 

Oder auch:

do_something_with(++b); 

jedoch std::mutex::lock(), unlock()std::thread(), std::async(), std::future::get() und so weiter sind Zäune. Der Compiler "weiß", dass er Lasten nicht neu ordnen und speichern (lesen und schreiben) darf, sodass die Operation auf der anderen Seite des Zauns endet, an der Sie beim Schreiben des Codes angegeben haben.

1: 
2: m.lock(); <--- This is a fence 
3: b += 1; <--- So this load/store operation may not move above line 2 
4: m.unlock(); <-- Nor may it be moved below this line 

Stellen Sie sich vor, was passieren würde, wenn dies nicht der Fall war:

(Neu geordnete Code)

thread1: int a = b + 1; 
    <--- Here another thread precedes us and executes the same block of code 
    thread2: int a = b + 1; 
    thread2: m.lock(); 
    thread2: b = a; 
    thread2: m.unlock(); 
thread1: m.lock(); 
thread1: b = a; 
thread1: m.unlock(); 
thread1:do_something_with(a); 
thread2:do_something_with(a); 

Wenn Sie es durch folgen, werden Sie sehen, dass b hat jetzt die falsche Wert, weil der Compiler den Code schneller gemacht hat.

... und das sind nur die Compileroptimierungen. std::mutex usw. verhindert auch, dass die Speichercaches Lasten neu anordnen und auf eine "optimalere" Weise speichern, was in einer Single-Thread-Umgebung gut wäre, in einem System mit mehreren Kernen (d. H. Jedem modernen PC oder Telefon) jedoch katastrophal. Die Kosten für Thread A müssen geleert werden, bevor Thread B die gleichen Daten liest, und das Leeren von Caches in den Arbeitsspeicher ist im Vergleich zum Cache-Speicherzugriff schrecklich langsam.

Es gibt Kosten für diese Sicherheit. Aber c'est la vie. Nur so kann die gleichzeitige Ausführung sichergestellt werden.

Deshalb bevorzugen wir, dass, wenn möglich, in einem SMP-System jeder Thread seine eigene Kopie von Daten hat, auf der er arbeiten kann. Wir möchten nicht nur die Zeit in einer Schleuse minimieren, sondern auch die Anzahl der Überfahrten.

Ich könnte weiter über die Modifikatoren std::memory_order sprechen, aber das ist ein dunkles und gefährliches Loch, das Experten oft falsch verstehen und in denen Anfänger keine Hoffnung haben, es richtig zu machen.

+4

Ihr letzter Satz ist eine Untertreibung. :) – Mysticial

1

Im Hinblick auf die Ressourcennutzung, Pflege der Compiler konnte nicht weniger über mutex.lock(). Es ist Aufgabe des Programmierers, sicherzustellen, dass der Ressourcenzugriff im Rahmen der ordnungsgemäßen Ver-/Entriegelung erfolgt.

Allerdings kümmert sich der Compiler über einen Mutex in einer Weise, dass es nicht zu optimieren weg bestimmte Konstrukte, die weiß, dass es sonst der Fall wäre - aber Sie sind wahrscheinlich in diesem Augenblick nicht interessiert.

6

"Mutex" ist die Abkürzung für "wechselseitiger Ausschluss"; Die richtige Disziplin für die Verwendung eines Mutex besteht darin, es zu sperren, bevor Code eingegeben wird, der die Variablen ändert, die zwischen Threads geteilt werden, und sie zu entsperren, wenn dieser Codeabschnitt fertig ist. Wenn ein Thread den Mutex sperrt, wird jeder andere Thread, der versucht, ihn zu sperren, blockiert, bis der Thread, der den Mutex besitzt, ihn entsperrt. Das bedeutet, dass sich immer nur ein Thread in einem Code befindet, der die gemeinsam genutzten Variablen ändern kann. Der Rest dessen, was ein Mutex tut, beruht auf einer Compiler-Magie auf einer bestimmten Ebene: Er verhindert auch, dass der Compiler Lasten und Datenspeicher innerhalb des geschützten Codes nach außerhalb verschiebt und umgekehrt, was für den geschützten Bereich notwendig ist Code, um geschützt zu bleiben.

3

Ein Mutex ist eine spezielle Implementierung eines semaphore. Insbesondere ist es ein binärer Semaphor.

Eine Semaphore (offensichtlich in einem Informatik-Kontext) kann als Integer-Variable und Hardware oder Software (Betriebssystem) primitive, die Atom (kann nicht unterbrochen werden) implementiert werden.

Stellen Sie sich vor etwas wie (pseudo-Höhe Level-Code):

int mutex = 1; // The mutex is free when created (free=1, occupied=0). 

// in a concurrency block 
{ 
    :try-again 

    // test if mutex is 1 (is free). 
    if (mutex > 0) { 

    // mutex WAS free so now I set as occupied (set 0) 
    --mutex; 
    // Now I've 'acquired' the mutex and since the mutex is 0 or 1 
    // only me can be here. 

    // Do something in mutual exclusion 
    // Done. 
    // Unlock the mutex 
    ++mutex; 
    // Now the mutex is free so other threads can acquire it. 

    } else { 
    // Mutex is 0 so I tried but it's already occupied. 
    // Block the thread and try again. 
    goto try-again; 
    } 

} 

Offensichtliche in einer reinen High-Level-Sprache, kann dieser Ansatz nicht funktionieren, weil ein Thread unterbrochen werden könnte, nachdem es der Mutex getestet wird frei und bevor es als besetzt gesetzt werden kann.

Aus diesem Grunde ist die Semaphore und so Mutex mit Hilfe von primitiven Befehlen implementiert werden, die diesen „Test and Set“ -Operation in „einem Takt“ (atomar) implementieren.

Ein Beispiel ist die test-and-set primitiv.

+0

Weil ich verbrannt wurde, indem ich Mutex und binäre Semaphore als austauschbar behandle, gibt es einen Unterschied. In vielen Threading-Bibliotheken (z. B. Pthreads) kann ein Mutex nur durch den Thread entsperrt werden, der ihn gesperrt hat, während ein Semaphor von jedem Thread "entsperrt" werden kann. –

3

Es gibt keine solche Klugheit, und die korrekte Funktion des Programmierers liegt in der Verantwortung des Programmierers.

Ein Mutex ist wie eine abschließbare Tür an einem Haus, das keine Wände hat.

Alles, was Sie damit tun können, ist zu verhindern, dass andere das Haus betreten durch die Tür, wenn es gesperrt ist.
Die Tür ist nur nützlich, wenn jeder zustimmt, das Haus ausschließlich durch die Tür zu betreten, und wenn die Tür verschlossen ist, warten Sie auf die Person im Inneren, um die Tür zu öffnen und zu verlassen.
Nichts verbietet einem schlechten Menschen, das Haus durch die nicht vorhandenen Wände zu betreten, außer der Vereinbarung, dass Sie nicht sollten.

+1

Die Cleverness ist in der Tatsache, dass der Optimierer und Codegenerator des Compilers die implizierten Speicherzäune erkennt und respektiert. –

+0

Egal, ohne Wände haben Sie nicht viel von einem Haus. – immibis

Verwandte Themen