Multithreaded-Paranoia

Lesezeit: 7 Minuten

Benutzer-Avatar
geschickt_code

Dies ist eine komplexe Frage, bitte überlegen Sie sorgfältig, bevor Sie antworten.

Betrachten Sie diese Situation. Zwei Threads (ein Leser und ein Schreiber) greifen auf eine einzige globale zu int. Ist das sicher? Normalerweise würde ich ohne nachzudenken antworten, ja!

Allerdings scheint mir, dass Herb Sutter das nicht glaubt. In seinen Artikeln über effektive Parallelität diskutiert er a fehlerhafte sperrfreie Warteschlange und die korrigierte Version.

Am Ende des ersten Artikels und am Anfang des zweiten Artikels diskutiert er eine selten beachtete Eigenschaft von Variablen, die Schreibreihenfolge. Ints sind atomar, gut, aber Ints sind nicht unbedingt geordnet, was jeden lock-freien Algorithmus zerstören könnte, einschließlich meines obigen Szenarios. Ich stimme voll und ganz zu, dass der einzige Weg zu Garantie Korrektes Multithreading-Verhalten auf allen gegenwärtigen und zukünftigen Plattformen ist die Verwendung von Atomic (auch bekannt als Speicherbarrieren) oder Mutexe.

Meine Frage; Ist Write Re-odering jemals ein Problem auf echter Hardware? Oder ist die Multithread-Paranoia nur pedantisch?
Was ist mit klassischen Einprozessorsystemen?
Was ist mit einfacheren RISC-Prozessoren wie einem eingebetteten Power-PC?

Klärung: Ich bin mehr daran interessiert, was Herr Sutter über die Hardware (Prozessor/Cache)-Umordnungsvariable gesagt hat. Ich kann den Optimierer daran hindern, Code mit Compilerschaltern zu brechen oder die Assembly nach der Kompilierung manuell zu inspizieren. Ich würde jedoch gerne wissen, ob die Hardware den Code in der Praxis immer noch durcheinander bringen kann.

  • Interessant. Meine erste Reaktion ist “wenn ich mir nicht sicher bin, würde ich es nicht wagen”

    – svrist

    3. Januar 2009 um 19:49 Uhr

  • Ja, hier genauso. “Normalerweise würde ich ohne nachzudenken antworten, nein!”. Es hängt alles davon ab, an welche Hardware Sie gewöhnt sind – auf einigen Plattformen können Sie nur mit “flüchtig” davonkommen, auf anderen jedoch nicht. Wenn Sie daran gewöhnt sind, portablen Code zu schreiben, nehmen Sie immer das „schlechteste“ mögliche Verhalten an.

    – Steve Jessop

    4. Januar 2009 um 13:42 Uhr

  • … was in diesem Fall Multi-CPU mit nicht kohärenten Speicher-Caches ist. Dann müssen Sie davon ausgehen, dass Änderungen in einem Thread, auch mit Volatilität, vorgenommen werden noch nie ohne Speicherbarriere an andere Threads weitergeben. Faustregel: Verschiedene Threads laufen auf verschiedenen Planeten und interagieren nur, wenn sie dazu gezwungen werden.

    – Steve Jessop

    4. Januar 2009 um 13:46 Uhr

  • Was hat die Montage damit zu tun? Es ist nicht der Compiler, der neu ordnet, sondern die Mikrocode-Logik auf der CPU selbst. Sicherlich könnte der Compiler seine eigene Neuordnung vornehmen, aber selbst wenn Sie dies mit der Assemblierung umgangen haben, sind Sie nicht aus dem Wald.

    – ApplePieIstGut

    5. Januar 2009 um 14:58 Uhr

Ihre Idee, die Baugruppe zu inspizieren, ist nicht gut genug; die Neuordnung kann auf Hardwareebene erfolgen.

Um Ihre Frage zu beantworten: “Ist dies jemals ein Problem bei Lesehardware:” Ja! Tatsächlich bin ich selbst auf dieses Problem gestoßen.

Ist es in Ordnung, das Problem mit Einprozessorsystemen oder anderen Sonderfällen zu umgehen? Ich würde mit “nein” argumentieren, denn in fünf Jahren müssen Sie vielleicht doch auf Multi-Core laufen, und dann wird es schwierig (unmöglich?), all diese Orte zu finden.

Eine Ausnahme: Software, die für eingebettete Hardwareanwendungen entwickelt wurde, bei denen Sie tatsächlich die vollständige Kontrolle über die Hardware haben. Tatsächlich habe ich in solchen Situationen auf zB einem ARM-Prozessor so “geschummelt”.

Yup – Verwenden Sie Speicherbarrieren, um die Neuordnung von Anweisungen zu verhindern, wo dies erforderlich ist. In einigen C++-Compilern wurde das Schlüsselwort volatile erweitert, um implizite Speicherbarrieren für jeden Lese- und Schreibvorgang einzufügen – aber dies ist keine portable Lösung. (Ebenso mit den Interlocked* win32 APIs). Vista fügt sogar einige neue, feinkörnigere Interlocked-APIs hinzu, mit denen Sie die Lese- oder Schreibsemantik festlegen können.

Leider hat C++ ein so lockeres Speichermodell, dass jede Art von Code wie dieser bis zu einem gewissen Grad nicht portierbar ist und Sie verschiedene Versionen für verschiedene Plattformen schreiben müssen.

  • FWIW, C++0x wird einen portablen Mechanismus zum Schreiben von Thread-sicherem Code einführen (inspiriert von der boost.thread-Bibliothek).

    – Shog9

    3. Januar 2009 um 20:07 Uhr

  • Halleluja! Natürlich wird es noch 5-10 Jahre dauern, bis C++0x-Code als portierbar angesehen werden kann…

    – Sonnenfinsternis

    3. Januar 2009 um 20:20 Uhr

  • Aber werden die c++0x-Funktionen das Problem des Speichermodells lösen? Dies ist per se kein Threading-Problem, daher sehe ich nicht, was diese neuen Funktionen (oder die vorhandenen in Boost) hier zu bieten haben. Dies ist ein Problem mit der Befehlsreihenfolge.

    – ApplePieIstGut

    5. Januar 2009 um 15:01 Uhr

  • Yup – C++0x führt ein besser definiertes Speichermodell sowie atomare <> Typen ein, die explizit ohne Verwendung von Sperren geändert werden können. Sehen open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2427.html und open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2429.htm

    – Sonnenfinsternis

    5. Januar 2009 um 17:16 Uhr

Wie Sie sagten, benötigen Sie aufgrund der Neuordnung auf Cache- oder Prozessorebene tatsächlich eine Art Speicherbarriere, um eine ordnungsgemäße Synchronisierung sicherzustellen, insbesondere für Multiprozessoren (und insbesondere auf Nicht-x86-Plattformen). (Ich gehe davon aus, dass Einzelprozessorsysteme diese Probleme nicht haben, aber zitieren Sie mich nicht dazu – ich bin sicherlich eher geneigt, auf Nummer sicher zu gehen und den synchronisierten Zugriff trotzdem durchzuführen.)

  • Die Probleme treten sogar bereits auf Einzelprozessorsystemen auf. Zum Beispiel sind PowerPC 60x-basierte Kerne perfekt in der Lage, E/A neu zu ordnen, da sie mehrere Ausführungseinheiten in jedem Kern haben. Dies ist ausdrücklich der Grund, warum die Befehle EIEIO, SYNC und ISYNC benötigt werden.

    – Großer Jeff

    3. Januar 2009 um 20:04 Uhr

  • Ja, selbst wenn der Code auf einem Einzelprozessor funktionieren würde, wäre es ein fragiles Codedesign, das auf mysteriöse Weise fehlschlägt, wenn Sie auf eine Maschine mit mehreren Prozessoren aufrüsten.

    – Bill Karwin

    3. Januar 2009 um 20:21 Uhr

  • Oder sogar ein intelligenterer Einzelprozessor. Beim Multithreading kann erwartet werden, dass sich alles, was nicht explizit garantiert wird, irgendwann in der Zukunft auf mysteriöse Weise ändert.

    – Sonnenfinsternis

    3. Januar 2009 um 20:22 Uhr

  • Ja, ich verstehe nicht, warum dies auf Multi-Core-Single-Proc-Maschinen beschränkt werden muss. Der springende Punkt ist, dass auf einer Single-Core-Single-Proc-Maschine Anweisungen auf Mikrocode-Ebene neu geordnet werden können.

    – ApplePieIstGut

    5. Januar 2009 um 15:03 Uhr

Wir sind auf das Problem gestoßen, allerdings auf Itanium-Prozessoren, bei denen die Befehlsumordnung aggressiver ist als bei x86/x64.

Die Lösung bestand darin, eine Interlocked-Anweisung zu verwenden, da es (damals) keine Möglichkeit gab, dem Compiler zu sagen, dass er einfach nur eine Schreibsperre nach der Zuweisung haben sollte.

Wir brauchen wirklich eine Spracherweiterung, um damit sauber umzugehen. Die Verwendung von volatile (falls vom Compiler unterstützt) ist zu grobkörnig für die Fälle, in denen Sie versuchen, so viel Leistung wie möglich aus einem Codestück herauszuholen.

Benutzer-Avatar
Henk

Ist das jemals ein Problem auf echter Hardware?

Absolut, besonders jetzt mit der Umstellung auf mehrere Kerne für aktuelle und zukünftige CPUs. Wenn Sie auf geordnete Atomarität angewiesen sind, um Funktionen in Ihrer Anwendung zu implementieren, und Sie diese Anforderung nicht über Ihre gewählte Plattform oder die Verwendung von Synchronisierungsprimitiven garantieren können, finden Sie unter alle Bedingungen, dh der Kunde wechselt von einer Single-Core-CPU zu einer Multi-Core-CPU, dann warten Sie nur darauf, dass ein Problem auftritt.

Zitat aus dem erwähnten Artikel von Herb Sutter (zweiter)

Geordnete atomare Variablen werden auf gängigen Plattformen und Umgebungen unterschiedlich geschrieben. Zum Beispiel:

  • volatile in C#/.NET, wie in volatile int.
  • volatile oder * Atomic* in Java, wie in volatile int, AtomicInteger.
  • atomic<T> in C++0x, dem bevorstehenden ISO-C++-Standard, wie in atomic<int>.

Ich habe nicht gesehen, wie C++0x geordnete Atomizität implementiert, daher kann ich nicht angeben, ob das kommende Sprachfeature eine reine Bibliotheksimplementierung ist oder auch auf Änderungen an der Sprache angewiesen ist. Sie können den Vorschlag überprüfen, um zu sehen, ob er als nicht standardmäßige Erweiterung in Ihre aktuelle Toolkette integriert werden kann, bis der neue Standard verfügbar ist, er ist möglicherweise sogar bereits für Ihre Situation verfügbar.

Benutzer-Avatar
Norman Ramsey

Es ist ein Problem auf echter Hardware. Ein Freund von mir arbeitet für IBM und verdient seinen Lebensunterhalt hauptsächlich damit, solche Probleme in Kundencodes aufzuspüren.

Wenn Sie sehen möchten, wie schlimm es werden kann, suchen Sie nach wissenschaftlichen Arbeiten zum Java-Speichermodell (und jetzt auch zum C++-Speichermodell). Angesichts der Neuordnung, die echte Hardware leisten kann, ist der Versuch herauszufinden, was in einer Hochsprache sicher ist, ein Albtraum.

Benutzer-Avatar
Rick

Nein, das ist nicht sicher, und es gibt echte Hardware, die dieses Problem aufweist, zum Beispiel ermöglicht das Speichermodell im PowerPC-Chip der Xbox 360, dass Schreibvorgänge neu geordnet werden. Dies wird durch das Fehlen von Barrieren in der Intrinsic verschärft, siehe diesen Artikel weiter msdn für mehr Details.

1371870cookie-checkMultithreaded-Paranoia

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy