Was ist effizienter, grundlegende Mutex-Sperre oder atomare Ganzzahl?


75

Für etwas Einfaches wie einen Zähler, wenn mehrere Threads die Anzahl erhöhen. Ich habe gelesen, dass Mutex-Sperren die Effizienz verringern können, da die Threads warten müssen. Für mich wäre ein Atomzähler am effizientesten, aber ich habe gelesen, dass es sich im Grunde genommen im Grunde genommen um ein Schloss handelt. Ich bin also verwirrt, wie einer effizienter sein kann als der andere.


Sollte diese Antwort für alle Plattformen und Programmiersprachen gelten, die pthreads oder eine Teilmenge unterstützen? Ich verstehe die Beziehungen zwischen Pthreads, Betriebssystemen und Programmiersprachen nicht vollständig, aber es scheint, dass diese Beziehungen relevant sein könnten.
snow_abstraction

Antworten:


52

Atomic Operations nutzen die Prozessorunterstützung (Anweisungen vergleichen und austauschen) und verwenden überhaupt keine Sperren, wohingegen Sperren stärker vom Betriebssystem abhängig sind und beispielsweise unter Win und Linux eine unterschiedliche Leistung erbringen.

Sperren setzen die Thread-Ausführung tatsächlich aus, wodurch CPU-Ressourcen für andere Aufgaben frei werden, aber beim Stoppen / Neustarten des Threads ein offensichtlicher Overhead beim Kontextwechsel entsteht. Im Gegenteil, Threads, die atomare Operationen versuchen, warten nicht und versuchen es bis zum Erfolg (sogenanntes Besetzt-Warten), sodass sie keinen Overhead für die Kontextumschaltung verursachen, aber auch keine CPU-Ressourcen freisetzen.

Zusammenfassend lässt sich sagen, dass atomare Operationen im Allgemeinen schneller sind, wenn die Konkurrenz zwischen Threads ausreichend gering ist. Sie sollten auf jeden Fall ein Benchmarking durchführen, da es keine andere zuverlässige Methode gibt, um zu wissen, was der geringste Overhead zwischen Kontextwechsel und Warten auf Besetzt ist.


47

Wenn Sie einen Zähler haben, für den atomare Operationen unterstützt werden, ist dieser effizienter als ein Mutex.

Technisch gesehen sperrt das Atom den Speicherbus auf den meisten Plattformen. Es gibt jedoch zwei verbessernde Details:

  • Es ist unmöglich, einen Thread während der Speicherbussperre anzuhalten, aber es ist möglich, einen Thread während einer Mutex-Sperre anzuhalten. Auf diese Weise erhalten Sie eine sperrenfreie Garantie (die nichts über das Nicht-Sperren aussagt - es garantiert nur, dass mindestens ein Thread Fortschritte macht).
  • Mutexe werden schließlich mit Atomics implementiert. Da Sie mindestens eine atomare Operation zum Sperren eines Mutex und eine atomare Operation zum Entsperren eines Mutex benötigen, dauert das Mutex-Sperren selbst in den besten Fällen mindestens zweimal.

Es ist wichtig zu verstehen, dass es davon abhängt, wie gut der Compiler oder Interpreter die Plattform unterstützt, um die besten Maschinenanweisungen (in diesem Fall sperrfreie Anweisungen) für die Plattform zu generieren. Ich denke, das ist es, was @Cort Ammon mit "unterstützt" meinte. Einige Mutexe können auch Garantien für den Fortschritt oder die Fairness einiger oder aller Threads geben, die nicht durch einfache atomare Anweisungen erstellt wurden.
snow_abstraction

17

Eine minimale (standardkonforme) Mutex-Implementierung erfordert zwei Grundbestandteile:

  • Eine Möglichkeit, eine Zustandsänderung zwischen Threads atomar zu übermitteln (der 'gesperrte' Zustand)
  • Speicherbarrieren, um durch den Mutex geschützte Speicheroperationen zu erzwingen, damit sie im geschützten Bereich bleiben.

Es gibt keine Möglichkeit, es einfacher zu machen, da die Beziehung zum Synchronisieren mit dem C ++ - Standard dies erfordert.

Eine minimale (korrekte) Implementierung könnte folgendermaßen aussehen:

class mutex {
    std::atomic<bool> flag{false};

public:
    void lock()
    {
        while (flag.exchange(true, std::memory_order_relaxed));
        std::atomic_thread_fence(std::memory_order_acquire);
    }

    void unlock()
    {
        std::atomic_thread_fence(std::memory_order_release);
        flag.store(false, std::memory_order_relaxed);
    }
};

Aufgrund seiner Einfachheit (es kann den Ausführungsthread nicht aussetzen) ist es wahrscheinlich, dass diese Implementierung bei geringen Konflikten a übertrifft std::mutex. Aber selbst dann ist leicht zu erkennen, dass jedes durch diesen Mutex geschützte Ganzzahlinkrement die folgenden Operationen erfordert:

  • ein atomicGeschäft, um den Mutex freizugeben
  • ein atomicVergleichen und Austauschen (Lesen, Ändern, Schreiben), um den Mutex zu erhalten (möglicherweise mehrmals)
  • ein ganzzahliges Inkrement

Wenn Sie dies mit einem Standalone vergleichen, der mit einem std::atomic<int>einzelnen (bedingungslosen) Lese-, Änderungs- und Schreibvorgang (z. B. fetch_add) inkrementiert wird , ist zu erwarten, dass eine atomare Operation (unter Verwendung des gleichen Ordnungsmodells) den Fall übertrifft, in dem ein Mutex vorliegt benutzt.


8

Atomic Integer ist dort ein Objekt im Benutzermodus, da es viel effizienter ist als ein Mutex, der im Kernelmodus ausgeführt wird . Der Bereich der atomaren Ganzzahl ist eine einzelne Anwendung, während der Bereich des Mutex für alle auf dem Computer ausgeführten Software gilt.


1
Das ist fast wahr. Moderne Mutex-Implementierungen wie Futex von Linux nutzen in der Regel atomare Operationen, um zu vermeiden, dass auf dem schnellen Weg in den Kernel-Modus gewechselt wird. Solche Mutexe müssen nur dann in den Kernelmodus springen, wenn die atomare Operation die gewünschte Aufgabe nicht ausführen konnte (z. B. in dem Fall, in dem der Thread blockieren muss).
Cort Ammon

Ich denke, der Umfang einer atomaren Ganzzahl ist ein einzelner Prozess , was insofern von Bedeutung ist, als Anwendungen aus mehreren Prozessen bestehen können (z. B. Python-Multiprocessing für Parallelität).
weberc2


2

Die meisten Prozessoren unterstützen ein atomares Lesen oder Schreiben und häufig ein atomares cmp & swap. Dies bedeutet, dass der Prozessor selbst den neuesten Wert in einer einzelnen Operation schreibt oder liest und im Vergleich zu einem normalen Ganzzahlzugriff möglicherweise einige Zyklen verloren gehen, zumal der Compiler nicht annähernd so gut wie normal für atomare Operationen optimieren kann.

Auf der anderen Seite ist ein Mutex eine Anzahl von Codezeilen, die eingegeben und verlassen werden müssen, und während dieser Ausführung sind andere Prozessoren, die auf denselben Speicherort zugreifen, vollständig blockiert, was eindeutig einen großen Aufwand für sie bedeutet. In nicht optimiertem High-Level-Code sind der Mutex-Ein- / Ausgang und der Atom-Funktionsaufruf. Bei Mutex wird jedoch jeder konkurrierende Prozessor gesperrt, während Ihre Mutex-Eingabefunktion zurückkehrt und Ihre Exit-Funktion gestartet wird. Für Atomic ist nur die Dauer der eigentlichen Operation gesperrt. Durch die Optimierung sollten diese Kosten gesenkt werden, jedoch nicht alle.

Wenn Sie versuchen, ein Inkrement zu erstellen, unterstützt Ihr moderner Prozessor wahrscheinlich das atomare Inkrementieren / Dekrementieren, was großartig sein wird.

Ist dies nicht der Fall, wird es entweder mit dem Prozessor Atomic Cmp & Swap oder mit einem Mutex implementiert.

Mutex:

get the lock
read
increment
write
release the lock

Atomic cmp & Swap:

atomic read the value
calc the increment
do{
   atomic cmpswap value, increment
   recalc the increment
}while the cmp&swap did not see the expected value

Diese zweite Version hat also eine Schleife [falls ein anderer Prozessor den Wert zwischen unseren atomaren Operationen erhöht, sodass der Wert nicht mehr übereinstimmt und das Inkrement falsch wäre], die lang werden kann [wenn es viele Konkurrenten gibt], aber im Allgemeinen immer noch schneller sein sollte als die Mutex-Version, aber die Mutex-Version kann es diesem Prozessor ermöglichen, die Task zu wechseln.


1

Mutexist eine Semantik auf Kernel-Ebene, die auch auf der Seite gegenseitigen Ausschluss bietet Process level. Beachten Sie, dass dies hilfreich sein kann, um den gegenseitigen Ausschluss über Prozessgrenzen hinweg und nicht nur innerhalb eines Prozesses (für Threads) zu erweitern. Es ist teurer.

Atomic Counter AtomicIntegerbasiert beispielsweise auf CAS und versucht normalerweise, die Operation auszuführen, bis sie erfolgreich ist. Grundsätzlich rasen oder konkurrieren in diesem Fall Threads, um den Wert atomar zu erhöhen / zu verringern. Hier sehen Sie möglicherweise gute CPU-Zyklen, die von einem Thread verwendet werden, der versucht, mit einem aktuellen Wert zu arbeiten.

Da Sie den Zähler beibehalten möchten, ist AtomicInteger \ AtomicLong das Beste für Ihren Anwendungsfall.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.