Muss ich atomisch verwenden, wenn ich bereits von mutex bewacht bin?

8

Gegeben der Code in diesen Beitrag , um ihn zu implementieren Semaphore verwendet nur atomic<> und mutex .

Ich bin nur neugierig, da count bereits von updateMutex geschützt ist, ist atomic<> notwendig?

%Vor%

Ohne atomic , ich denke, der Konstruktor würde ein Synchronisationsproblem bekommen. Die Zuweisung zu count ist möglicherweise nicht sichtbar, wenn andere Threads sie direkt nach der Konstruktion verwenden.

Wenn ja, was ist mit size ? Muss es auch durch atomic<> geschützt werden?

Oder atomic<> ist völlig unbrauchbar, da sowohl size als auch count sichtbar sind, egal wann andere Threads sie verwenden.

Danke!

    
Xin Huang 29.12.2013, 09:52
quelle

3 Antworten

5

Es werden mehrere Fragen gestellt. Alle erfordern, dass das zugrundeliegende Konzept verstanden wird: Sie haben eine Datenrasse , wenn ein Objekt von mindestens einem Thread geschrieben wird, auf den von einem anderen Thread zugegriffen (gelesen oder geschrieben) wird und der Schreibvorgang und der Zugriff nicht sind synchronisiert. Die formale Definition von Datenrennen ist in 1.10 [intro.multithread] Absatz 21:

  

Die Ausführung eines Programms enthält eine Datenrasse , wenn sie zwei widersprüchliche Aktionen in verschiedenen Threads enthält, von denen mindestens einer nicht atomar ist und keiner vor dem anderen. [...]

Ein Programm, das ein Datenrennen enthält, hat ein undefiniertes Verhalten, d. h. das Programm muss sicherstellen, dass es Datenrennen-frei ist. Nun zu den verschiedenen Fragen:

  1. Ist es notwendig, die Synchronisation im Konstruktor zu verwenden?

    Es hängt davon ab, ob das Objekt gleichzeitig von verschiedenen Threads zugegriffen werden kann, während es noch im Aufbau ist. Der einzige Fall, in dem ich mir den gleichzeitigen Zugriff auf ein im Bau befindliches Objekt vorstellen kann, ist während der statischen Initialisierung, bei der bereits mehrere Threads gestartet wurden, die auf das gemeinsame Objekt zugreifen. Aufgrund der schwachen Einschränkungen in der Reihenfolge der Konstruktion für globale Objekte kann ich mir nicht vorstellen, dass globale Objekte trotzdem verwendet werden und die Konstruktion von lokalen static -Objekten der Funktion wird durch die Implementierung synchronisiert. Sonst würde ich erwarten, dass ein Verweis auf das Objekt über Threads geteilt wird, wobei ein geeignet synchronisierter Mechanismus verwendet wird. Das heißt, ich würde das System so entwerfen, dass der Konstruktor keine Synchronisation benötigt.

  2. Es gibt bereits eine Sperre. Bedeutet das, dass count kein Atom sein muss.

    Da auf count in der acquire() -Funktion zugegriffen wird, bevor die Sperre erhalten wird, wäre dies ein unsynchronisierter Zugriff auf ein Objekt, das von einem anderen Thread geschrieben wurde, dh Sie hätten ein Datenrennen und daher undefiniert Verhalten. Der count muss atomar sein.

  3. Sollen auch size synchronisiert werden?

    Das size -Member wird nur im Konstruktor von Semaphore geändert und es könnte sinnvoll sein, dies zu erzwingen, indem Sie es tatsächlich zu einem const -Member machen. Unter der Annahme, dass das Objekt während des Aufbaus nicht gleichzeitig aufgerufen wird (siehe 1. oben), besteht bei einem Zugriff auf size kein Potential für einen Datenrennen.

Beachten Sie, dass Sie die Mitglieder% lock() und unlock() des Mutex nicht ungeschützt verwenden sollten. Stattdessen sollten Sie std::lock_guard<std::mutex> oder std::unique_lock<std::mutex> verwenden, möglicherweise mit einem Hilfsblock. Diese zwei Klassen garantieren, dass ein erworbenes Schloss immer freigegeben wird. Ich würde auch fragen, ob ein beschäftigtes Warten auf einen Semaphor, der sowieso eine Sperre erwirbt, der richtige Weg ist.

    
Dietmar Kühl 29.12.2013, 14:16
quelle
7

Ich denke, der wahre Grund dafür, dass count ein atomic<int> ist, ist gelesen in aquire() außerhalb des mutex -geschützten Bereichs In dieser Zeile:

%Vor%

Ohne atomic darf das Kompilieren davon ausgehen, dass es einmal gelesen wird und es nicht nach geänderten Werten von anderen Threads fragt.

    
Daniel Frey 29.12.2013 10:36
quelle
3

Ja. Es besteht ein theoretisches Risiko, dass:

count = 0;

im Konstruktor wird von einem anderen Threading, das auf einer anderen CPU ausgeführt wird, nicht rechtzeitig für einen nachfolgenden Aufruf von acquire() oder release() beobachtet. Die Möglichkeit dieses Ereignisses ist wahrscheinlich verschwindend gering, da der Konstruktor abgeschlossen werden muss, um das Semaphor-Objekt zu verwenden, und irgendwie muss ein anderer Thread das Objekt erreichen.

Dies bedeutet, dass die Ansicht der anderen CPU des durch count belegten Speichers zwischen den CPUs nicht synchronisiert ist und eine andere CPU einen alten (z. B. nicht initialisierten) Wert lesen könnte.

Wenn hier standardmäßig ein std::atomic<int> verwendet wird, werden Speicherbarrieren um die Lasten (in diesem Fall überladene Operatoren) erzeugt und gespeichert. Standardmäßig ist dies ultrakonservativ. .

Sie können auch den Mutex im Konstruktor für den gleichen Effekt sperren und entsperren - aber das ist noch teurer.

Man muss sagen, dass dies eine ziemlich unangenehme Art ist, einen Zähl-Semaphor zu implementieren - aber es war eine Interviewfrage und hat als solche eine Menge Facetten.

    
marko 29.12.2013 10:08
quelle

Tags und Links