Wann beginnen Sie bei der Entwicklung einer Software mit dem Nachdenken / Entwerfen der gleichzeitigen Abschnitte?


8

Nach dem Prinzip, nicht zu früh zu optimieren, frage ich mich, an welchem ​​Punkt beim Design / der Entwicklung einer Software Sie über die Möglichkeiten der Parallelität nachdenken.

Ich kann mir gut vorstellen, dass eine Strategie darin besteht, eine einzelne Thread-Anwendung zu schreiben und durch Profilerstellung Abschnitte zu identifizieren, die Kandidaten für eine parallele Ausführung sind. Eine andere Strategie, von der ich ein wenig gesehen habe, besteht darin, die Software nach Aufgabengruppen zu betrachten und die unabhängigen Aufgaben parallel zu gestalten.

Ein Grund für die Frage ist natürlich, dass Sie, wenn Sie bis zum Ende warten und die Software nur für den gleichzeitigen Betrieb umgestalten, die Dinge möglicherweise so schlecht wie möglich strukturieren und eine wichtige Aufgabe vor sich haben.

Welche Erfahrungen haben dazu beigetragen, festzustellen, wann Sie Parallelisierung in Ihrem Design berücksichtigen?

Antworten:


7

Das Besondere an Thread-Aufgaben ist, dass Sie eine hohe Kohäsion, eine geringe Kopplung und eine gute Kapselung wünschen. Interessanterweise sind diese Ziele auch für Single-Threaded-Anwendungen würdig. Ich hatte heute eine Aufgabe, die ich ursprünglich nicht parallelisieren wollte, aber als ich das tat, ging es nur darum, eine Funktion in run()umzubenennen und ihre Bezeichnung zu ändern.

Nicht zu früh zu optimieren bedeutet, nicht alles "nur für den Fall" in einen Thread zu setzen, aber Sie sollten sich auch nicht in eine architektonische Ecke streichen, damit es bei Bedarf zu schwierig ist, zu optimieren.


Ja, wiedereintretende Funktionen, Arbeitswarteschlangen und dergleichen können sowohl in Single-Threaded- als auch in Multithread-Anwendungen hilfreich sein, ermöglichen aber auch eine gute Erweiterbarkeit für die spätere Parallelverarbeitung. Das spart später viel Kopfschmerzen bei Synchronisationsproblemen.
Coder

2

Java-Programmierer sollten die CallableSchnittstelle für Arbeitseinheiten nutzen. Wenn Ihre gesamte Anwendung aus einer Schleife zum Erstellen von Callables, zum Versenden aller Einheiten an einen Executor und zum Erledigen von Aufgaben nach der Generierung besteht, haben Sie etwas, das sehr einfach in serielle Verarbeitung umgewandelt werden kann: "Drei Arbeitswarteschlangen" und "Alle erledigen" sofort "einfach durch Auswahl des richtigen Testamentsvollstreckers.

Wir passen dieses Muster langsam an, da es sehr häufig vorkommt, dass der serielle Ansatz an einem Punkt zu langsam wird und wir es dann trotzdem tun müssen.


1

Es variiert mit dem Projekt. Manchmal ist es sehr einfach zu erkennen, was parallel gemacht werden kann: Vielleicht verarbeitet Ihr Programm Stapel von Dateien. Angenommen, die Verarbeitung jeder Datei ist völlig unabhängig von allen anderen Dateien, sodass es ziemlich offensichtlich ist, dass Sie jeweils eine Datei oder 10 oder 100 verarbeiten können und keiner dieser Jobs die andere beeinflusst.

Es wird etwas komplizierter, wenn die potenziellen parallelen Jobs nicht gleich sind. Wenn Sie eine Bilddatei verarbeiten, können Sie einen Job haben, der ein Histogramm erstellt, einen anderen, der eine Miniaturansicht erstellt, und möglicherweise einen anderen, der EXIF-Metadaten extrahiert, und dann einen endgültigen Job, der die Ausgabe all dieser Jobs übernimmt und sie in einer Datenbank speichert. In diesem Beispiel ist möglicherweise nicht klar, ob diese parallel ausgeführt werden sollen oder ob sie ausgeführt werden sollen (der letzte Job muss warten, bis alle vorherigen Jobs erfolgreich abgeschlossen wurden).

Nach meinen Erfahrungen besteht der einfachste Weg, etwas zu parallelisieren, darin, nach Prozessen zu suchen, die so unabhängig wie möglich ausgeführt werden können (wie im ersten Beispiel), und mit diesen zu beginnen. Ich würde nur versuchen, das zweite Beispiel parallel laufen zu lassen, wenn ich dachte, ich würde damit einen signifikanten Leistungsgewinn erzielen.


1

Sie müssen von Anfang an Parallelität in Ihre Anwendung einbauen. Normalerweise würde ich als Optimierung zustimmen, dass es bis später belassen werden sollte, wenn es nicht von Natur aus offensichtlich ist. Das Problem ist, dass für die Parallelität im schlimmsten Fall möglicherweise eine Neugestaltung Ihrer Anwendung von Grund auf erforderlich ist. Bei einigen Systemen ist es praktisch unmöglich, die Parallelität in Angriff zu nehmen. Ein einfaches Beispiel hierfür sind Systeme, die Daten gemeinsam nutzen, beispielsweise die Simulations- und Renderingaspekte eines Spiels.


0

Ich würde sagen, dass Threading ein Teil der Architektur der Anwendung ist. Es ist also eines der ersten Dinge, an die ich denken muss.

Wenn ich beispielsweise eine GUI-Anwendung ausführe, ist der GUI-Code Single-Threaded, sodass lange laufende Aufgaben (z. B. XML-Verarbeitung) die GUI blockieren und stattdessen in einem Hintergrund-Thread ausgeführt werden sollten.

Beispielsweise wäre ein Server entweder threadbasiert, wobei jede Anforderung von einem neuen Thread verarbeitet wird, oder der Server könnte ereignisgesteuert sein und nur einen Thread pro CPU-Kern verwenden. Andererseits sollten Aufgaben mit langer Laufzeit in a ausgeführt werden Hintergrund-Thread oder in kleinere Aufgaben unterteilt werden.


0

Mit der Art und Weise, wie ich mit Dingen umgehe, ist Multithreading kostenlos und im Nachhinein relativ einfach anzuwenden. Aber ich denke zuerst an Daten. Ich weiß nicht, ob dies für alle Domains funktioniert, aber ich werde versuchen zu beschreiben, wie ich vorgehe.

Zunächst geht es also um die gröbsten Daten, die für die Software erforderlich sind und die häufig verarbeitet werden. Wenn es sich um ein Spiel handelt, bei dem es sich möglicherweise um Netze, Geräusche, Bewegungen, Partikelemitter, Lichter, Texturen oder ähnliche Dinge handelt. Und natürlich gibt es viel zu überlegen, wenn Sie sich nur mit Maschen befassen und darüber nachdenken, wie sie dargestellt werden sollen, aber das überspringen wir vorerst. Im Moment denken wir auf der breitesten architektonischen Ebene.

Und mein erster Gedanke ist: "Wie vereinheitlichen wir die Darstellung all dieser Dinge, damit wir für all diese Arten von Dingen ein relativ einheitliches Zugriffsmuster erreichen können?" Und mein erster Gedanke könnte sein, jede einzelne Art von Dingen in einem eigenen zusammenhängenden Array mit einer kostenlosen Art von Liste zu speichern, um freie Räume zurückzugewinnen. Dies führt dazu, dass die API vereinheitlicht wird, sodass wir beispielsweise leichter denselben Code zum Serialisieren von Netzen verwenden können wie Lichter und Texturen, zumindest was den Zugriff auf diese Komponenten betrifft. Je mehr wir vereinheitlichen können, wie alles dargestellt wird, desto mehr nimmt der Code, der auf diese Dinge zugreift, eine einheitliche Form an.

Das ist cool. Jetzt können wir auch mit 32-Bit-Indizes auf diese Dinge verweisen und nur die Hälfte des Speichers eines 64-Bit-Zeigers beanspruchen. Und hey, wir können jetzt Schnittpunkte in linearer Zeit setzen, wenn wir ein paralleles Bitset zuordnen können, z. B. können wir Daten auch sehr billig parallel zu einem dieser Dinge zuordnen, da wir alles indizieren. Oh, und dieses Bitset kann uns einen Satz sortierter Indizes zurückgeben, die in sequentieller Reihenfolge durchlaufen werden, um die Speicherzugriffsmuster zu verbessern, ohne dass dieselbe Cache-Zeile mehrmals in einer einzelnen Schleife neu geladen werden muss. Wir können jeweils 64-Bit testen. Wenn nicht alle 64-Bit gesetzt sind, können wir 64 Elemente gleichzeitig überspringen. Wenn alle festgelegt sind, können wir sie alle gleichzeitig verarbeiten. Wenn einige gesetzt sind, aber nicht alle, können wir mithilfe von FFS-Anweisungen schnell bestimmen, welche Bits gesetzt sind.

Aber oh warte, das ist ziemlich teuer, wenn wir nur ein paar hundert Dinge aus Zehntausenden von Dingen mit Daten verknüpfen wollen. Verwenden wir stattdessen ein spärliches Array wie folgt:

Geben Sie hier die Bildbeschreibung ein

Und hey, jetzt, da wir alles in spärlichen Arrays gespeichert und indiziert haben, wäre es ziemlich einfach, dies zu einer dauerhaften Datenstruktur zu machen.

Geben Sie hier die Bildbeschreibung ein

Jetzt können wir billigere Funktionen ohne Nebenwirkungen schreiben, da sie nicht tief kopieren müssen, was sich nicht geändert hat.

Und hier habe ich bereits einen Spickzettel erhalten, nachdem ich etwas über ECS-Engines gelernt habe, aber jetzt wollen wir uns überlegen, welche Art von allgemeinen Funktionen für jeden Komponententyp ausgeführt werden sollen. Wir können diese "Systeme" nennen. Das "SoundSystem" kann "Sound" -Komponenten verarbeiten. Jedes System ist eine umfassende Funktion, die mit einem oder mehreren Datentypen arbeitet.

Geben Sie hier die Bildbeschreibung ein

Dies führt zu vielen Fällen, in denen für einen bestimmten Komponententyp im Allgemeinen nur ein oder zwei Systeme darauf zugreifen. Hmm, das scheint sicher die Gewindesicherheit zu verbessern und den Gewindekonflikt auf ein Minimum zu reduzieren.

Außerdem versuche ich darüber nachzudenken, wie man homogene Datenübertragungen durchführt. Anstatt wie:

for each thing:
    play with it
    cuddle it
    kill it

Ich versuche es in mehrere, einfachere Durchgänge aufzuteilen:

for each thing:
    play with it
for each thing:
    cuddle it
for each thing:
    kill it

Das erfordert manchmal das Speichern eines Zwischenzustands für den nächsten homogenen verzögerten Durchlauf, aber ich fand, dass dies mir wirklich hilft, den Code beizubehalten und zu überlegen, da ich weiß, dass jede Schleife eine einfachere, einheitlichere Logik hat. Und hey, das scheint die Thread-Sicherheit zu vereinfachen und Thread-Konflikte zu reduzieren.

Und Sie machen einfach so weiter, bis Sie feststellen, dass Sie eine Architektur haben, die wirklich einfach mit dem Vertrauen in die Thread-Sicherheit und -Korrektheit zu parallelisieren ist, aber zunächst mit dem Fokus, Datenrepräsentationen zu vereinheitlichen, vorhersehbarere Speicherzugriffsmuster zu haben und zu reduzieren Speichernutzung, Vereinfachung des Kontrollflusses zu homogeneren Durchläufen, Reduzierung der Anzahl von Funktionen in Ihrem System, die Nebenwirkungen verursachen, ohne dass sehr teure Kosten für das Kopieren entstehen, Vereinheitlichung Ihrer API usw.

Wenn Sie all diese Dinge kombinieren, erhalten Sie in der Regel ein System, das die Menge des gemeinsam genutzten Status minimiert, in dem Sie auf ein Design gestoßen sind, das für Parallelität wirklich freundlich ist. Und wenn ein Status gemeinsam genutzt werden muss, gibt es häufig keine großen Konflikte, wenn es günstig ist, eine Synchronisierung zu verwenden, ohne einen Thread-Verkehrsstau zu verursachen, und außerdem kann er häufig von Ihrer zentralen Datenstruktur verarbeitet werden, die die Darstellung vereinheitlicht ausgerechnet im System, damit Sie keine Thread-Synchronisierungen auf hundert verschiedene Stellen anwenden müssen, sondern nur auf eine Handvoll.

Wenn wir nun einen Drilldown zu einer der komplexeren Komponenten wie Netzen durchführen, wiederholen wir den gleichen Entwurfsprozess, indem wir zunächst über die Daten nachdenken. Und wenn wir das richtig machen, können wir möglicherweise sogar die Verarbeitung eines einzelnen Netzes problemlos parallelisieren, aber das breitere Architekturdesign, das wir bereits erstellt haben, ermöglicht es uns, die Verarbeitung mehrerer Netze zu parallelisieren.

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.