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:
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.
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.
quelle
Antworten:
Ü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.
quelle
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.
quelle
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
ReaderWriterLockSlim
Klasse, 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
delegate
Objekt 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.quelle
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:
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:
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.quelle