2013-08-02 3 views
6

Wenn in einem C- oder C++ - Programm 2 Threads dieselbe globale Variable verwenden, müssen Sie die Variable über einen Mutex sperren.In welchen Fällen muss ich eine Variable vom gleichzeitigen Zugriff sperren?

Aber in welchen Fällen genau?

  1. Gewinde 1: lesen Thread 2: lesen
  2. Gewinde 1: schreiben Thema 2: schreiben Thema: 2: schreiben

Natürlich müssen Sie im Fall sperren

  • Thread 1 lesen 3 aber was ist mit den anderen 2 Fällen? Was passiert in Fall 2 (mit atomaren Operationen)? Gibt es eine Art von Zugriffsverletzung oder erhält Thread 2 nur den alten Wert? Ich bin ein wenig verwirrt darüber, weil Speicher und Register auf der Hardwareebene nicht gleichzeitig zugänglich sind (in normaler PC-Hardware) oder haben wir irgendeine Art von parallelen CPUs mit parallelen Busleitungen zu parallelen RAM-Chips?

  • +11

    Fälle 2 und 3, d. H. Jede Situation, in der mindestens ein Thread mit mindestens einem anderen Thread liest oder schreibt. – juanchopanza

    +0

    Sie müssen nicht immer sperren, wenn Sie auf Daten von verschiedenen Threads zugreifen. Schauen Sie sich atomare Operationen an, und besonders bei C++ 11's 'std :: atomic' – nijansen

    +0

    Und ja, um Ihren letzten Satz zu adressieren, kann es bei Multicore-CPUs mehrere Kopien jedes Wertes in den verschiedenen Cache-Ebenen geben. – Hulk

    Antwort

    8

    Denken Sie nur daran, was in jedem der Fälle passieren kann. Betrachten wir nur die Race Condition: Es ist einfach und es reicht uns, die Konsequenzen zu sehen.

    In Fall 1 wird die Variable nicht geändert, so dass beide Threads unabhängig von der Reihenfolge denselben Wert lesen. Also im Grunde ist hier nichts falsch.

    Fälle 2 und 3 sind schlechter. Nehmen wir an, Sie haben eine Race-Bedingung und wissen nicht, auf welchen der Threads früher zugegriffen wird. Das bedeutet:

    Für den Fall 2: Der Wert der Variablen am Ende aller Operationen ist in Ordnung (es wird der Wert von Thread 1 geschrieben), aber Thread 2 kann einen alten Wert der Variablen erhalten, die möglicherweise einen Absturz oder andere Probleme verursachen.

    Für Fall 3: Der Endwert der Variablen ist nicht vorhersagbar, da es davon abhängt, welcher Thread die Schreiboperation zuletzt ausführen wird.

    Für die Fälle 2 und 3 kann es auch vorkommen, dass einer der Threads versucht, auf die Variable zuzugreifen, während sie sich in einem inkonsistenten Zustand befindet, und Sie möglicherweise einige von einem der Threads gelesene Daten löschen Fall 2) oder sogar Daten in der Variablen nach Abschluss aller Operationen abräumen.

    So ja, Schloss für Fälle 2 und 3

    +3

    Fall 2 kann ziemlich schlecht sein: die Variable könnte in einem inkonsistenten Zustand sein, wenn sie von Thread 2 gelesen wird. – juanchopanza

    +0

    @juanchopanza, ja, das stimmt (auch für Fall 3), aber ich habe es nicht erwähnt, um die Antwort nicht zu komplizieren . Soll ich es dann zu der Antwort hinzufügen oder es so lassen wie es ist? – SingerOfTheFall

    +4

    Ich denke, es ist notwendig, dies zu der Antwort hinzuzufügen, es ist das Hauptproblem mit nicht-atomaren Operationen. Ihre Antwort scheint Atomizität zu implizieren. Beachten Sie außerdem, dass das Sperren keine bestimmte Zugriffsreihenfolge garantiert. – juanchopanza

    3

    Die Regeln für die Verriegelung sind einfach:

    Wenn Sie auf eine Variable schreiben, gleichzeitig von einem anderen Thread zugegriffen wird, dann müssen Sie die richtige Sequenzierung Operationen (zum Beispiel durch Sperren).

    Mit dieser einfachen Regel können wir leicht jedem der Fälle bewerten:

    1. Kein Schreiben, keine Notwendigkeit für die Synchronisation
    2. schreiben, gleichzeitigen Zugriff, müssen Synchronisation
    3. schreiben, gleichzeitigen Zugriff , brauche Synchronisation

    Und was passiert, wenn Sie nicht sperren?

    Nun, formal ist dies undefined Verhalten. Bedeutet, dass wir es nicht wissen. Obwohl die einzig mögliche Antwort hilft, das Ausmaß des Problems zu erfassen.

    Auf der Maschinenebene, was passieren kann, ist entweder:

    • bei Lese: Zugriff auf einen veralteten Wert
    • bei Lese: Zugriff auf einen Teilwert (nur die Hälfte bei aktualisiert die Zeit, die Sie
    • bei Schreib aussehen): der Endwert ist ein Sammelsurium (bei Bit-Ebene) des Wertes

    ... und lassen Sie sich nicht vergessen, dass, wenn Sie eine falsche Zeiger/Größe gelesen es kann zu einem cr führen Asche.

    Auf der Compiler-Ebene kann der Compiler optimieren, als ob der Thread einzigen Zugriff hatte. In diesem Fall kann es bedeuten:

    • Entfernen "redundant", heißt es: verwandeln while (flag) in if (flag) while (true) ...
    • Entfernen "ungenutzt", schreibt
    • ...

    Das Vorhandensein von Speichern Zäune und explizite Synchronisationsanweisungen (die die Verwendung von Mutexen einführt) verhindern diese Optimierungen.

    2

    Die Regel ist einfach: Wenn ein Objekt zugegriffen wird (Lesen oder Schreiben) durch mehr als ein Thread, und ist von jedem Thread modifiziert, dann alle Zugriffe müssen synchronisiert werden. Andernfalls haben Sie undefined Verhalten.

    0

    Es ist nicht ganz so einfach, wie es scheinen mag, mit Ausnahme von zwei Fällen:

    1. Wenn es nur Leser und keine Autoren, Sie nicht benötigen, immer zu synchronisieren.
    2. Wenn mehrere Threads schreiben und mindestens einer von ihnen eine Lese-Modify-Write-Operation ausführt (wie ++x;), müssen Sie immer synchronisieren, oder Sie erhalten völlig unvorhersehbare Ergebnisse.

    In allen anderen Fällen, wenn Sie mindestens ein Schriftsteller und einen Leser (oder mehrere Autoren) haben, können Sie in der Regel (mit wenigen Ausnahmen) benötigen Zugang zu synchronisieren, aber nicht unbedingt immer, und nicht immer auf die strengste Art und Weise.

    Es hängt sehr viel davon ab, was Sie brauchen. Einige Anwendungen benötigen strikte sequenzielle Konsistenz über Threads (und manchmal müssen Sie sogar Lock Fairness). Einige Anwendungen werden gleich gut funktionieren, aber mit einer viel besseren Leistung, wenn sie nur innerhalb des gleichen Threads "happen-before" -Garantien haben. Andere Anwendungen brauchen nicht einmal so viel und sind völlig zufrieden mit entspannten Operationen oder ohne irgendwelche Garantien.

    Zum Beispiel dieser „typische“ Implementierung eines Worker-Thread hat einen Autor und einen Leser:

    // worker thread 
    running = true; 
    while(running) { task = pull_task(); execute(task); } 
    
    // main thread exit code 
    running = false; 
    join(workerthread); 
    

    Dies ohne Synchronisation sehr gut funktioniert. Ja, pedantisch ist es undefiniert, wann oder wie sich der Wert von running ändert, aber in Wirklichkeit macht das keinen Unterschied. Es gibt keine Möglichkeit, dass der Speicherort einen "zufälligen" Zwischenwert hat, und es spielt keine Rolle, ob die Änderung einige Dutzend Sekunden früher oder später sichtbar wird, da der Worker-Thread höchstwahrscheinlich ohnehin damit beschäftigt ist, eine Aufgabe auszuführen im schlimmsten Fall nimmt es einige Millisekunden später die Änderung auf. Bei der nächsten Iteration wird der Worker-Thread die Änderung übernehmen und wird beendet.
    Die SPS-Fast-Forward-Queue, die vor einigen Jahren in Dr. Dobb veröffentlicht wurde, arbeitete nach einem ähnlichen Prinzip, nur mit Zeigern.

    Eine gute und umfassende Lektüre über die vielen verschiedenen Synchronisationsmodi und ihre Auswirkungen finden Sie in der GCC documentation.

    Verwandte Themen