Ich habe gehört, dass i ++ keine thread-sichere Anweisung ist, da es in der Assembly darauf hinausgeht, den ursprünglichen Wert irgendwo als temporär zu speichern, ihn zu erhöhen und dann zu ersetzen, was durch einen Kontextwechsel unterbrochen werden könnte.
Ich wundere mich jedoch über ++ i. Soweit ich das beurteilen kann, würde sich dies auf eine einzelne Assembly-Anweisung reduzieren, z. B. 'add r1, r1, 1', und da es sich nur um eine Anweisung handelt, wäre sie durch einen Kontextwechsel nicht unterbrechbar.
Kann jemand klarstellen? Ich gehe davon aus, dass eine x86-Plattform verwendet wird.
c++
c
multithreading
Samoz
quelle
quelle
Antworten:
Du hast falsch gehört. Es kann durchaus sein, dass dies
"i++"
für einen bestimmten Compiler und eine bestimmte Prozessorarchitektur threadsicher ist, aber in den Standards überhaupt nicht vorgeschrieben ist. Da Multithreading nicht Teil der ISO C- oder C ++ - Standards (a) ist , können Sie nichts als threadsicher betrachten, basierend auf dem, was Ihrer Meinung nach kompiliert wird.Es ist durchaus machbar, dass
++i
eine beliebige Reihenfolge wie folgt kompiliert werden kann:Das wäre auf meiner (imaginären) CPU, die keine Anweisungen zum Inkrementieren des Speichers enthält, nicht threadsicher. Oder es kann klug sein und es kompilieren in:
wo
lock
deaktiviert undunlock
aktiviert Interrupts. Aber selbst dann ist dies in einer Architektur mit mehr als einer dieser CPUs, die sich den Speicher teilen, möglicherweise nicht threadsicher (lock
möglicherweise werden nur Interrupts für eine CPU deaktiviert).Die Sprache selbst (oder Bibliotheken dafür, wenn sie nicht in die Sprache integriert ist) bietet threadsichere Konstrukte, und Sie sollten diese verwenden, anstatt von Ihrem Verständnis (oder möglicherweise Missverständnissen) davon abzuhängen, welcher Maschinencode generiert wird.
Dinge wie Java
synchronized
undpthread_mutex_lock()
(für C / C ++ unter einigen Betriebssystemen verfügbar) sind das, worauf Sie achten müssen (a) .(a) Diese Frage wurde gestellt, bevor die Standards C11 und C ++ 11 abgeschlossen wurden. Diese Iterationen haben jetzt Threading-Unterstützung in die Sprachspezifikationen eingeführt, einschließlich atomarer Datentypen (obwohl sie und Threads im Allgemeinen zumindest in C optional sind ).
quelle
Sie können weder für ++ i noch für i ++ eine pauschale Aussage treffen. Warum? Erwägen Sie, eine 64-Bit-Ganzzahl auf einem 32-Bit-System zu erhöhen. Sofern die zugrunde liegende Maschine nicht über eine Quad-Wort-Anweisung "Laden, Inkrementieren, Speichern" verfügt, erfordert das Inkrementieren dieses Werts mehrere Anweisungen, von denen jede durch einen Thread-Kontextwechsel unterbrochen werden kann.
Zusätzlich,
++i
ist nicht immer "eine zum Wert hinzufügen". In einer Sprache wie C erhöht das Inkrementieren eines Zeigers tatsächlich die Größe des Objekts, auf das gezeigt wird. Das heißt, wenni
es sich um einen Zeiger auf eine 32-Byte-Struktur handelt, werden++i
32 Bytes hinzugefügt. Während fast alle Plattformen einen atomaren Befehl "Inkrementwert an Speicheradresse" haben, haben nicht alle einen atomaren Befehl "Wert an Speicheradresse einen beliebigen Wert hinzufügen".quelle
Sie sind beide thread-unsicher.
Eine CPU kann nicht direkt mit dem Speicher rechnen. Dies geschieht indirekt, indem der Wert aus dem Speicher geladen und mit CPU-Registern berechnet wird.
i ++
++ i
In beiden Fällen gibt es eine Racebedingung, die zu einem unvorhersehbaren i-Wert führt.
Angenommen, es gibt zwei gleichzeitige ++ i-Threads, die jeweils das Register a1 bzw. b1 verwenden. Und mit Kontextumschaltung wie folgt ausgeführt:
Im Ergebnis wird i nicht zu i + 2, sondern zu i + 1, was falsch ist.
Um dies zu beheben, stellen Moden-CPUs während des Intervalls, in dem eine Kontextumschaltung deaktiviert ist, eine Art LOCK, UNLOCK-CPU-Anweisungen bereit.
Verwenden Sie unter Win32 InterlockedIncrement (), um i ++ für die Thread-Sicherheit auszuführen. Es ist viel schneller als sich auf Mutex zu verlassen.
quelle
Wenn Sie in einer Multi-Core-Umgebung sogar ein Int über mehrere Threads hinweg gemeinsam nutzen, benötigen Sie geeignete Speicherbarrieren. Dies kann bedeuten, dass Sie gesperrte Anweisungen verwenden (siehe z. B. InterlockedIncrement in Win32) oder eine Sprache (oder einen Compiler) verwenden, die bestimmte threadsichere Garantien bietet. Bei der Neuanordnung von Anweisungen auf CPU-Ebene sowie bei Caches und anderen Problemen sollten Sie, sofern Sie nicht über diese Garantien verfügen, nicht davon ausgehen, dass die gemeinsame Nutzung von Threads sicher ist.
Bearbeiten: Eine Sache, die Sie bei den meisten Architekturen annehmen können, ist, dass Sie, wenn Sie mit richtig ausgerichteten Einzelwörtern arbeiten, nicht ein einzelnes Wort erhalten, das eine Kombination aus zwei Werten enthält, die zusammengefügt wurden. Wenn zwei Schreibvorgänge übereinander erfolgen, gewinnt einer und der andere wird verworfen. Wenn Sie vorsichtig sind, können Sie dies nutzen und feststellen, dass entweder ++ i oder i ++ in der Situation mit einem einzelnen Schreiber / mehreren Lesern threadsicher sind.
quelle
Wenn Sie ein atomares Inkrement in C ++ wünschen, können Sie C ++ 0x-Bibliotheken (den
std::atomic
Datentyp) oder etwas wie TBB verwenden.Es gab einmal eine Zeit, in der die GNU-Codierungsrichtlinien sagten, das Aktualisieren von Datentypen, die in ein Wort passen, sei "normalerweise sicher", aber dieser Rat ist für SMP-Maschinen
falsch, für einige Architekturenfalsch und bei Verwendung eines optimierenden Compilers falsch.So verdeutlichen Sie den Kommentar "Aktualisieren des Datentyps mit einem Wort":
Es ist möglich, dass zwei CPUs auf einem SMP-Computer im selben Zyklus auf denselben Speicherort schreiben und dann versuchen, die Änderung auf die anderen CPUs und den Cache zu übertragen. Selbst wenn nur ein Datenwort geschrieben wird, sodass die Schreibvorgänge nur einen Zyklus dauern, werden sie gleichzeitig ausgeführt, sodass Sie nicht garantieren können, welcher Schreibvorgang erfolgreich ist. Sie erhalten keine teilweise aktualisierten Daten, aber ein Schreibvorgang verschwindet, da es keine andere Möglichkeit gibt, diesen Fall zu behandeln.
Das richtige Vergleichen und Austauschen koordiniert zwischen mehreren CPUs, aber es gibt keinen Grund zu der Annahme, dass jede variable Zuweisung von Ein-Wort-Datentypen das Vergleichen und Austauschen verwendet.
Ein optimierender Compiler hat zwar keinen Einfluss darauf, wie ein Laden / Speichern kompiliert wird, kann sich jedoch ändern, wenn das Laden / Speichern erfolgt. Dies kann zu ernsthaften Problemen führen, wenn Sie erwarten, dass Ihre Lese- und Schreibvorgänge in derselben Reihenfolge erfolgen, in der sie im Quellcode angezeigt werden ( Das bekannteste doppelt überprüfte Sperren funktioniert in Vanilla C ++ nicht.
HINWEIS Meine ursprüngliche Antwort besagte auch, dass die 64-Bit-Architektur von Intel beim Umgang mit 64-Bit-Daten fehlerhaft war. Das ist nicht wahr, also habe ich die Antwort bearbeitet, aber meine Bearbeitung behauptete, PowerPC-Chips seien kaputt. Dies gilt auch für das Einlesen von Sofortwerten (dh Konstanten) in Register (siehe die beiden Abschnitte mit dem Namen "Laden von Zeigern" unter Listing 2 und Listing 4). Es gibt jedoch eine Anweisung zum Laden von Daten aus dem Speicher in einem Zyklus (
lmw
), sodass ich diesen Teil meiner Antwort entfernt habe.quelle
Unter x86 / Windows in C / C ++ sollten Sie nicht davon ausgehen, dass es threadsicher ist. Sie sollten InterlockedIncrement () und InterlockedDecrement () verwenden, wenn Sie atomare Operationen benötigen.
quelle
Wenn Ihre Programmiersprache nichts über Threads aussagt und dennoch auf einer Multithread-Plattform ausgeführt wird, wie kann ein Sprachkonstrukt threadsicher sein?
Wie andere betonten: Sie müssen jeden Multithread-Zugriff auf Variablen durch plattformspezifische Aufrufe schützen.
Es gibt Bibliotheken, die die Plattformspezifität abstrahieren, und der kommende C ++ - Standard hat sein Speichermodell angepasst, um mit Threads fertig zu werden (und somit die Thread-Sicherheit zu gewährleisten).
quelle
Selbst wenn es auf eine einzelne Assembly-Anweisung reduziert wird und der Wert direkt im Speicher erhöht wird, ist es immer noch nicht threadsicher.
Beim Inkrementieren eines Werts im Speicher führt die Hardware eine "Lese-Änderungs-Schreib" -Operation aus: Sie liest den Wert aus dem Speicher, erhöht ihn und schreibt ihn zurück in den Speicher. Die x86-Hardware kann nicht direkt im Speicher erhöht werden. Der RAM (und die Caches) können nur Werte lesen und speichern, nicht ändern.
Angenommen, Sie haben zwei separate Kerne, entweder auf separaten Sockets oder gemeinsam für einen einzelnen Socket (mit oder ohne gemeinsam genutzten Cache). Der erste Prozessor liest den Wert, und bevor er den aktualisierten Wert zurückschreiben kann, liest der zweite Prozessor ihn. Nachdem beide Prozessoren den Wert zurückgeschrieben haben, wurde er nur einmal und nicht zweimal erhöht.
Es gibt eine Möglichkeit, dieses Problem zu vermeiden. x86-Prozessoren (und die meisten Multi-Core-Prozessoren, die Sie finden werden) können diese Art von Hardwarekonflikt erkennen und sequenzieren, sodass die gesamte Lese-, Änderungs- und Schreibsequenz atomar erscheint. Da dies jedoch sehr kostspielig ist, erfolgt dies nur auf Anforderung des Codes, auf x86 normalerweise über das
LOCK
Präfix. Andere Architekturen können dies auf andere Weise mit ähnlichen Ergebnissen tun. Zum Beispiel Load-Linked / Store-Conditional und Atomic Compare-and-Swap (neuere x86-Prozessoren haben auch diesen letzten).Beachten Sie, dass die Verwendung
volatile
hier nicht hilft. Es teilt dem Compiler nur mit, dass die Variable möglicherweise extern geändert wurde, und das Lesen dieser Variablen darf nicht in einem Register zwischengespeichert oder optimiert werden. Der Compiler verwendet keine atomaren Grundelemente.Der beste Weg ist, atomare Grundelemente zu verwenden (sofern Ihr Compiler oder Ihre Bibliotheken über diese verfügen) oder das Inkrement direkt in der Assembly durchzuführen (unter Verwendung der richtigen atomaren Anweisungen).
quelle
Gehen Sie niemals davon aus, dass ein Inkrement zu einer atomaren Operation kompiliert wird. Verwenden Sie InterlockedIncrement oder ähnliche Funktionen auf Ihrer Zielplattform.
Bearbeiten: Ich habe gerade diese spezielle Frage nachgeschlagen und das Inkrement auf X86 ist auf Einzelprozessorsystemen atomar, aber nicht auf Multiprozessorsystemen. Die Verwendung des Sperrpräfixes kann es atomar machen, aber es ist viel portabler, nur InterlockedIncrement zu verwenden.
quelle
Gemäß dieser Assembler-Lektion auf x86 können Sie einem Speicherort atomar ein Register hinzufügen , sodass Ihr Code möglicherweise atomar '++ i' oder 'i ++' ausführen kann. Wie in einem anderen Beitrag erwähnt, wendet die C ansi keine Atomizität auf die '++' - Operation an, sodass Sie nicht sicher sein können, was Ihr Compiler generieren wird.
quelle
Der C ++ - Standard von 1998 hat nichts über Threads zu sagen, obwohl der nächste Standard (der in diesem oder im nächsten Jahr fällig ist) dies tut. Daher können Sie nichts Intelligentes über die Thread-Sicherheit von Vorgängen sagen, ohne sich auf die Implementierung zu beziehen. Es wird nicht nur der Prozessor verwendet, sondern auch die Kombination aus Compiler, Betriebssystem und Thread-Modell.
Ohne gegenteilige Dokumentation würde ich nicht davon ausgehen, dass eine Aktion threadsicher ist, insbesondere bei Mehrkernprozessoren (oder Multiprozessorsystemen). Ich würde Tests auch nicht vertrauen, da Thread-Synchronisationsprobleme wahrscheinlich nur zufällig auftreten.
Nichts ist threadsicher, es sei denn, Sie haben eine Dokumentation, die besagt, dass es sich um das von Ihnen verwendete System handelt.
quelle
Wirf i in den lokalen Thread-Speicher. es ist nicht atomar, aber es spielt dann keine Rolle.
quelle
AFAIK, Nach dem C ++ - Standard sind Lese- / Schreibvorgänge in einem
int
atomar.Dies alles ist jedoch, das undefinierte Verhalten, das mit einem Datenrennen verbunden ist, loszuwerden.
Es wird jedoch immer noch ein Datenrennen geben, wenn beide Threads versuchen, sich zu erhöhen
i
.Stellen Sie sich folgendes Szenario vor:
Lassen Sie
i = 0
zunächst:Thread A liest den Wert aus dem Speicher und speichert ihn in seinem eigenen Cache. Thread A erhöht den Wert um 1.
Thread B liest den Wert aus dem Speicher und speichert ihn in seinem eigenen Cache. Thread B erhöht den Wert um 1.
Wenn dies alles ein einzelner Thread ist, würden Sie
i = 2
in den Speicher bekommen.Bei beiden Threads schreibt jeder Thread seine Änderungen, sodass Thread A
i = 1
in den Speicher zurückschreibt und Thread Bi = 1
in den Speicher schreibt .Es ist gut definiert, es gibt keine teilweise Zerstörung oder Konstruktion oder irgendeine Art von Zerreißen eines Objekts, aber es ist immer noch ein Datenrennen.
Um atomar zu erhöhen,
i
können Sie verwenden:std::atomic<int>::fetch_add(1, std::memory_order_relaxed)
Eine entspannte Reihenfolge kann verwendet werden, da es uns egal ist, wo diese Operation stattfindet. Wir kümmern uns nur darum, dass die Inkrementierungsoperation atomar ist.
quelle
Sie sagen: "Es ist nur eine Anweisung, die durch einen Kontextwechsel nicht unterbrochen werden kann." - das ist alles gut und schön für eine einzelne CPU, aber was ist mit einer Dual-Core-CPU? Dann können wirklich zwei Threads gleichzeitig auf dieselbe Variable zugreifen, ohne dass Kontextwechsel erforderlich sind.
Ohne die Sprache zu kennen, ist die Antwort, sie zu testen.
quelle
Ich denke, wenn der Ausdruck "i ++" der einzige in einer Anweisung ist, der "++ i" entspricht, ist der Compiler klug genug, um keinen zeitlichen Wert usw. beizubehalten. Wenn Sie sie also austauschbar verwenden können (andernfalls haben Sie gewonnen Fragen Sie nicht, welche Sie verwenden sollen), es spielt keine Rolle, welche Sie verwenden, da sie fast gleich sind (mit Ausnahme der Ästhetik).
Selbst wenn der Inkrementoperator atomar ist, garantiert dies nicht, dass der Rest der Berechnung konsistent ist, wenn Sie nicht die richtigen Sperren verwenden.
Wenn Sie selbst experimentieren möchten, schreiben Sie ein Programm, bei dem N Threads gleichzeitig eine gemeinsame Variable M-mal inkrementieren ... Wenn der Wert kleiner als N * M ist, wurde ein gewisses Inkrement überschrieben. Probieren Sie es mit Vor- und Nachinkrement aus und sagen Sie es uns ;-)
quelle
Für einen Zähler empfehle ich die Verwendung der Vergleichs- und Austauschsprache, die sowohl nicht sperrend als auch threadsicher ist.
Hier ist es in Java:
quelle