Wie kann ich das Weiterleiten von Nachrichten zwischen Threads in einer Multithread-Engine vereinfachen?

18

Die C ++ - Engine, an der ich gerade arbeite, ist in mehrere große Threads unterteilt: Generieren (zum Erstellen meiner prozeduralen Inhalte), Spielen (für KI, Skripte, Simulationen), Physik und Rendern.

Die Threads kommunizieren miteinander über kleine Nachrichtenobjekte, die von Thread zu Thread weitergeleitet werden. Vor dem Ausführen eines Schritts verarbeitet ein Thread alle eingehenden Nachrichten - Aktualisierungen, um Objekte zu transformieren, hinzuzufügen und zu entfernen usw. Manchmal erstellt ein Thread (Generation) etwas (Art) und übergibt es einem anderen Thread (Rendering) für dauerhaften Besitz.

Früh im Prozess und ich habe ein paar Dinge bemerkt:

  1. Das Nachrichtensystem ist umständlich. Das Erstellen eines neuen Nachrichtentyps umfasst das Unterklassen der Basis-Nachrichtenklasse, das Erstellen einer neuen Aufzählung für den Typ und das Schreiben einer Logik, wie die Threads den neuen Nachrichtentyp interpretieren sollen. Es ist ein Geschwindigkeitsschub für die Entwicklung und neigt zu Tippfehlern. (Nebenbemerkung: Wenn ich daran arbeite, schätze ich, wie großartig dynamische Sprachen sein können!)

    Gibt es einen besseren Weg, dies zu tun? Sollte ich so etwas wie boost :: bind verwenden, um dies automatisch zu machen? Ich mache mir Sorgen, dass ich dadurch die Fähigkeit verliere, Nachrichten nach Typ oder etwas anderem zu sortieren. Ich bin mir nicht sicher, ob diese Art von Management überhaupt notwendig wird.

  2. Der erste Punkt ist wichtig, weil diese Threads so viel kommunizieren. Das Erstellen und Weitergeben von Nachrichten ist ein wichtiger Bestandteil der Umsetzung. Ich möchte dieses System rationalisieren, bin aber auch offen für andere Paradigmen, die genauso hilfreich sein könnten. Gibt es verschiedene Multithread-Designs, über die ich nachdenken sollte, um dies zu vereinfachen?

    Beispielsweise gibt es einige Ressourcen, die selten geschrieben, aber häufig aus mehreren Threads gelesen werden. Sollte ich offen sein für die Idee, gemeinsame Daten, die durch Mutexe geschützt sind, für alle Threads zugänglich zu machen?

Es ist das erste Mal, dass ich etwas entwerfe, das Multithreading von Grund auf berücksichtigt. In diesem frühen Stadium denke ich, dass es wirklich gut läuft (wenn man bedenkt), aber ich mache mir Sorgen um die Skalierung und meine eigene Effizienz bei der Implementierung neuer Dinge.

Raptormeat
quelle
6
Es gibt hier wirklich keine einzige gezielte Frage, und daher passt dieser Beitrag nicht zum Q & A-Stil dieser Site. Ich würde empfehlen, dass Sie Ihren Post in einzelne Posts aufteilen, einen pro Frage, und die Fragen neu ausrichten, damit sie sich auf ein bestimmtes Problem beziehen, das Sie tatsächlich haben, anstatt auf eine vage Sammlung von Tipps oder Ratschlägen.
2
Wenn Sie sich allgemeiner unterhalten möchten, empfehle ich Ihnen, diesen Beitrag in den Foren auf gamedev.net zu lesen . Wie Josh sagte, ist es ziemlich schwierig, Ihre "Frage" im StackExchange-Format zu beantworten, da es sich nicht um eine einzelne spezifische Frage handelt.
Cypher
Danke für das Feedback Jungs! Ich hatte gehofft, jemand mit mehr Wissen könnte eine einzige Ressource / Erfahrung / ein Paradigma haben, das mehrere meiner Probleme gleichzeitig angeht. Ich habe das Gefühl, dass eine große Idee meine verschiedenen Probleme zu einer Sache zusammenfassen könnte, die ich vermisse, und ich dachte, jemand mit mehr Erfahrung, als ich vielleicht erkannt hätte ... Aber vielleicht auch nicht, und trotzdem wurden Punkte gezogen !
Raptormeat
Ich habe Ihren Titel so umbenannt, dass er spezifischer für das Weiterleiten von Nachrichten ist, da Fragen vom Typ "Tipps" implizieren, dass kein bestimmtes Problem zu lösen ist (und daher würde ich heutzutage als "keine echte Frage" schließen).
Tetrad
Bist du sicher, dass du separate Threads für Physik und Gameplay benötigst? Diese beiden scheinen sehr eng miteinander verbunden zu sein. Es ist auch schwierig zu wissen, wie man Ratschläge gibt, ohne zu wissen, wie jeder von ihnen mit wem kommuniziert.
Nicol Bolas

Antworten:

10

Überlegen Sie sich, um Ihr allgemeines Problem zu lösen, wie Sie die Kommunikation zwischen Threads so weit wie möglich reduzieren können. Es ist besser, Synchronisierungsprobleme zu vermeiden, wenn Sie können. Dies kann erreicht werden, indem Ihre Daten doppelt gepuffert werden, eine Latenzzeit von einem Update eingeführt wird, die Arbeit mit gemeinsam genutzten Daten jedoch erheblich vereinfacht wird.

Haben Sie außerdem darüber nachgedacht, nicht nach Subsystemen zu fädeln, sondern stattdessen Thread-Spawning oder Thread-Pools zu verwenden, um nach Tasks zu fädeln? (Siehe dies in Bezug auf Ihr spezielles Problem für das Thread-Pooling.) In diesem kurzen Papier werden Zweck und Verwendung des Pool-Musters kurz umrissen. Siehe diese informativen Antwortenebenfalls. Wie dort erwähnt, verbessern Thread-Pools die Skalierbarkeit als Bonus. Und es heißt "einmal schreiben, überall verwenden", anstatt bei jedem Schreiben eines neuen Spiels oder einer neuen Engine Subsystem-basierte Threads zu benötigen, um gut zu spielen. Es gibt auch viele solide Thread-Pooling-Lösungen von Drittanbietern. Es ist einfacher, mit dem Thread-Laichen zu beginnen und sich später auf den Weg zu Thread-Pools zu machen, wenn der Aufwand für das Laichen und Löschen von Threads verringert werden muss.

Ingenieur
quelle
1
Gibt es Empfehlungen für bestimmte Thread-Pooling-Bibliotheken, die Sie auschecken sollten?
imre
Nick - vielen Dank für die Antwort. Zum ersten Punkt: Ich denke, das ist eine großartige Idee und wahrscheinlich die Richtung, in die ich gehen werde. Im Moment ist es früh genug, dass ich noch nicht weiß, was doppelt gepuffert werden muss. Ich werde das im Hinterkopf behalten, da sich das mit der Zeit verfestigt. Zu deinem zweiten Punkt - danke für den Vorschlag! Ja, die Vorteile von Thread-Aufgaben liegen auf der Hand. Ich werde Ihre Links lesen und darüber nachdenken. Nicht 100% sicher, ob es für mich funktioniert / wie es für mich funktioniert, aber ich werde definitiv ernsthaft darüber nachdenken. Vielen Dank!
Raptormeat
1
@imre Besuche die Boost-Bibliothek - sie haben Futures, die eine nette / einfache Art sind, sich diesen Dingen zu nähern.
Jonathan Dickinson
5

Sie haben nach verschiedenen Multi-Thread-Designs gefragt. Ein Freund von mir erzählte mir von dieser Methode, die ich für ziemlich cool hielt.

Die Idee ist, dass es 2 Kopien von jeder Spieleinheit geben würde (verschwenderisch, ich weiß). Eine Kopie wäre die aktuelle Kopie und die andere wäre die frühere Kopie. Die vorliegende Kopie ist streng schreibgeschützt und die vorherige Kopie ist streng schreibgeschützt . Beim Aktualisieren weisen Sie so vielen Threads Bereiche Ihrer Entitätsliste zu, wie Sie für richtig halten. Jeder Thread hat Schreibzugriff auf die aktuellen Kopien im zugewiesenen Bereich und jeder Thread hat Lesezugriff auf alle vorherigen Kopien der Entitäten und kann daher die zugewiesenen aktuellen Kopien unter Verwendung von Daten aus den vorherigen Kopien ohne Sperrung aktualisieren. Zwischen den einzelnen Bildern wird die aktuelle Kopie zur vorherigen Kopie, Sie möchten jedoch den Rollentausch durchführen.

John McDonald
quelle
4

Wir hatten das gleiche Problem, nur mit C #. Nachdem wir lange überlegt hatten, ob es einfach ist (oder nicht), neue Nachrichten zu erstellen, konnten wir am besten einen Code-Generator für sie erstellen. Es ist ein bisschen hässlich, aber brauchbar: Wenn nur eine Beschreibung des Nachrichteninhalts angegeben wird, werden Nachrichtenklassen, Aufzählungen, Code für die Behandlung von Platzhaltern usw. generiert.

Ich bin damit nicht ganz zufrieden, aber es ist besser, den ganzen Code von Hand zu schreiben.

In Bezug auf gemeinsame Daten lautet die beste Antwort natürlich "es kommt darauf an". Wenn einige Daten jedoch häufig gelesen werden und von vielen Threads benötigt werden, lohnt es sich, sie gemeinsam zu nutzen. Aus Gründen der Thread-Sicherheit ist es am besten, den Thread unveränderlich zu machen . Wenn dies jedoch nicht in Frage kommt, kann es sein , dass Mutex dies tut. In C # gibt es eine ReaderWriterLockSlimKlasse, die speziell für solche Fälle entwickelt wurde. Ich bin sicher, dass es ein C ++ - Äquivalent gibt.

Eine andere Idee für die Thread-Kommunikation, die wahrscheinlich Ihr erstes Problem löst, ist die Übergabe von Handlern anstelle von Nachrichten. Ich bin nicht sicher, wie ich das in C ++ herausfinden soll, aber in C # können Sie ein delegateObjekt an einen anderen Thread senden (wie in, es zu einer Art Nachrichtenwarteschlange hinzufügen) und diesen Delegaten tatsächlich vom empfangenden Thread aus aufrufen . Dies ermöglicht die Erstellung von "Ad-hoc" -Nachrichten vor Ort. Ich habe nur mit dieser Idee gespielt und sie nie in der Produktion ausprobiert, so dass sie sich in der Tat als schlecht herausstellen könnte.

Keine Ursache
quelle
Danke für all die tollen Infos! Das letzte Stück über Handler ähnelt dem, was ich über die Verwendung von Bindungen oder Funktoren zur Übergabe von Funktionen erwähnt habe. Die Idee gefällt mir irgendwie - vielleicht probiere ich sie aus und finde heraus, ob sie scheiße ist oder nicht: D Ich könnte damit beginnen, einfach eine CallDelegateMessage-Klasse zu erstellen und meinen Zeh ins Wasser zu tauchen.
Raptormeat
1

Ich bin nur in der Entwurfsphase eines eingefädelten Spielcodes, daher kann ich nur meine Gedanken teilen, keine tatsächliche Erfahrung. Vor diesem Hintergrund denke ich wie folgt:

  • Die meisten Spieldaten sollten für den schreibgeschützten Zugriff freigegeben werden .
  • Das Schreiben von Daten ist über eine Art Messaging möglich.
  • Um zu vermeiden, dass Daten aktualisiert werden, während ein anderer Thread sie liest, werden in der Spieleschleife zwei Phasen unterschieden: Lesen und Aktualisieren.
  • In der Lesephase:
  • Alle freigegebenen Daten sind für alle Threads schreibgeschützt.
  • Threads können Material (mit gewinde lokaler Speicher) und produzieren Aktualisierung berechnen Anfragen , die im Grunde Objekte Befehl / Meldung sind, in eine Warteschlange gestellt, die später aufgebracht werden.
  • In der Update-Phase:
  • Alle freigegebenen Daten sind schreibgeschützt. Daten sind in einem unbekannten / instabilen Zustand anzunehmen.
  • Hier werden Aktualisierungsanforderungsobjekte verarbeitet.

Ich denke (obwohl ich nicht sicher bin), dass dies theoretisch bedeuten sollte, dass während der Lese- und Aktualisierungsphase eine beliebige Anzahl von Threads gleichzeitig mit minimaler Synchronisierung ausgeführt werden kann. In der Lesephase schreibt niemand die gemeinsam genutzten Daten, sodass keine Probleme mit der Parallelität auftreten sollten. Die Update-Phase ist schwieriger. Parallele Aktualisierungen derselben Dateneinheit wären ein Problem, daher ist hier eine gewisse Synchronisierung angebracht. Ich könnte jedoch immer noch eine beliebige Anzahl von Update-Threads ausführen, solange sie mit verschiedenen Datensätzen arbeiten.

Insgesamt denke ich, dass sich dieser Ansatz gut für ein Thread-Pooling-System eignet. Die problematischen Teile sind:

  • Aktualisierungs-Threads synchronisieren (stellen Sie sicher, dass nicht mehrere Threads versuchen, denselben Datensatz zu aktualisieren).
  • Stellen Sie sicher, dass in der Lesephase kein Thread versehentlich freigegebene Daten schreiben kann. Ich fürchte, es gibt zu viel Platz für Programmierfehler, und ich bin mir nicht sicher, wie viele davon leicht von Debug-Tools erkannt werden könnten.
  • Schreiben Sie Code so, dass Sie sich nicht darauf verlassen können, dass Ihre Zwischenergebnisse sofort zum Lesen zur Verfügung stehen. Das heißt, Sie können nicht schreiben, x += 2; if (x > 5) ...wenn x gemeinsam genutzt wird. Sie müssen entweder eine lokale Kopie von x erstellen oder eine Aktualisierungsanforderung erstellen und die Bedingung erst in der nächsten Ausführung ausführen. Letzteres würde eine Menge zusätzlichen Thread-lokalen Code zur Beibehaltung des Status des Boilerplate bedeuten.
imre
quelle