Jobsystem. Übersicht von der anderen Seite

In der neuen Version von Unity im Jahr 2018 haben sie schließlich offiziell das neue Entity-Komponentensystem, kurz ECS , hinzugefügt, mit dem Sie nur mit ihren Daten arbeiten können, anstatt wie üblich mit den Komponenten des Objekts.

Ein zusätzliches Task-System bietet Ihnen die Möglichkeit, parallele Rechenleistung zu verwenden, um die Leistung Ihres Codes zu verbessern.

Zusammen bieten diese beiden neuen Systeme ( ECS und Job System ) eine neue Ebene der Datenverarbeitung.

Insbesondere werde ich in diesem Artikel nicht das gesamte ECS- System analysieren, das derzeit als separat herunterladbarer Satz von Tools in Einheit verfügbar ist, sondern nur das Task-System und dessen Verwendung außerhalb des ECS- Pakets berücksichtigen.

Neues System


Ursprünglich hätte Unity zuvor Multithread-Computing verwenden können, aber all dies musste vom Entwickler selbst erstellt werden, um die Probleme selbst zu lösen und die Fallstricke zu umgehen. Und wenn es früher notwendig war, direkt mit Dingen wie dem Erstellen von Threads, dem Schließen von Threads, Pools und der Synchronisierung zu arbeiten, fiel diese ganze Arbeit jetzt auf die Schultern der Engine, und der Entwickler selbst musste nur noch Aufgaben erstellen und ausführen.

Die Aufgaben


Um Berechnungen im neuen System durchzuführen, müssen Aufgaben verwendet werden, bei denen es sich um Objekte handelt, die aus Methoden und Daten für die Berechnung bestehen.

Wie alle anderen Daten im ECS- System werden auch Aufgaben im Jobsystem als Strukturen dargestellt, die eine von drei Schnittstellen erben.

Ijob


Die einfachste Task-Schnittstelle mit einer Execute- Methode, die nichts in Form von Parametern akzeptiert und nichts zurückgibt.

Die Aufgabe selbst sieht folgendermaßen aus:

Ijob
public struct JobStruct : IJob { public void Execute() {} } 


In der Execute- Methode können Sie die erforderlichen Berechnungen durchführen.

IJobParallelFor


Eine weitere Schnittstelle mit derselben Execute- Methode, die wiederum bereits den numerischen Parameterindex akzeptiert.

IJobParallelFor
 public struct JobStruct : IJobParallelFor { public void Execute(int index) {} } 


Diese IJobParallelFor- Schnittstelle bietet im Gegensatz zur IJob- Schnittstelle die Möglichkeit, eine Aufgabe mehrmals auszuführen und diese Ausführung nicht nur auszuführen, sondern in Blöcke aufzuteilen , die zwischen Threads verteilt werden.

Unverständlich? Mach dir darüber keine Sorgen, ich werde dir mehr erzählen.

IJobParallelForTransform


Und die letzte spezielle Schnittstelle, die, wie der Name schon sagt, für diese Transformationen des Objekts ausgelegt ist. Es enthält auch die Execute- Methode mit dem numerischen Parameterindex und dem TransformAccess- Parameter, in der sich Position, Größe und Drehung der Transformation befinden.

IJobParallelForTransform
 public struct JobStruct : IJobParallelForTransform { public void Execute(int index, TransformAccess transform) {} } 


Aufgrund der Tatsache, dass Sie nicht direkt in der Aufgabe mit Unity- Objekten arbeiten können, kann diese Schnittstelle Transformationsdaten nur als separate TransformAccess- Struktur verarbeiten.

Wenn Sie fertig sind und wissen, wie Aufgabenstrukturen erstellt werden, können Sie mit dem Üben fortfahren.

Aufgabenerfüllung


Lassen Sie uns eine einfache Aufgabe erstellen, die von der IJob- Oberfläche geerbt wurde , und sie abschließen. Dazu benötigen wir ein einfaches MonoBehaviour- Skript und die Struktur der Aufgabe selbst.

Testjob
 public class TestJob : MonoBehaviour { void Start() {} } 


Legen Sie dieses Skript nun auf einem Objekt in der Szene ab. Im selben Skript ( TestJob ) schreiben wir die Struktur der Aufgabe und vergessen nicht, die erforderlichen Bibliotheken zu importieren.

Simplejob
 using Unity.Jobs; public struct SimpleJob : IJob { public void Execute() { Debug.Log("Hello parallel world!"); } } 


Drucken Sie in der Execute- Methode beispielsweise eine einfache Zeile an die Konsole.

Fahren wir nun mit der Start- Methode des TestJob- Skripts fort, in der wir eine Instanz der Aufgabe erstellen und dann ausführen.

Testjob
 public class TestJob : MonoBehaviour { void Start() { SimpleJob job = new SimpleJob(); job.Schedule().Complete(); } } 


Wenn Sie alles wie im Beispiel gemacht haben, erhalten Sie nach dem Start des Spiels eine einfache Nachricht an die Konsole wie auf dem Bild.

Bild

Was hier passiert: Nach dem Aufrufen der Schedule- Methode platziert der Scheduler die Aufgabe im Handle und kann nun durch Aufrufen der Complete- Methode abgeschlossen werden.

Dies war ein Beispiel für eine Aufgabe, bei der einfach Text auf die Konsole gedruckt wurde. Damit eine Aufgabe parallele Berechnungen durchführen kann, muss sie mit Daten gefüllt werden.

Daten in der Aufgabe


Wie im ECS- System gibt es bei Aufgaben keinen Zugriff auf Unity- Objekte. Sie können das GameObject nicht in die Aufgabe aufnehmen und dort seinen Namen ändern. Sie können lediglich einige separate Objektparameter auf die Aufgabe übertragen, diese Parameter ändern und diese Änderungen nach Abschluss der Aufgabe wieder auf das Objekt anwenden.

Die Daten in der Aufgabe selbst unterliegen mehreren Einschränkungen: Erstens müssen es Strukturen sein, und zweitens dürfen es keine konvertierbaren Datentypen sein, dh Sie können nicht denselben Booleschen Wert oder dieselbe Zeichenfolge an die Aufgabe übergeben.

Simplejob
 public struct SimpleJob : IJob { public float a, b; public void Execute() { float result = a + b; Debug.Log(result); } } 


Und die Hauptbedingung: Auf Daten, die nicht in einem Container enthalten sind, kann nur innerhalb der Aufgabe zugegriffen werden!

Container


Bei der Arbeit mit Multithread-Computing müssen Daten zwischen Threads ausgetauscht werden. Um Daten in sie übertragen und im Task-System zurücklesen zu können, gibt es für diese Zwecke Container. Diese Container werden in Form gewöhnlicher Strukturen dargestellt, und ich arbeite nach dem Prinzip einer Brücke, über die Elementardaten zwischen Flüssen synchronisiert werden.

Es gibt verschiedene Arten von Behältern:
NativeArray . Der einfachste und am häufigsten verwendete Containertyp wird als einfaches Array mit fester Größe dargestellt.
NativeSlice . Ein weiterer Container - ein Array, wie aus der Übersetzung hervorgeht, dient dazu, das NativeArray in Stücke zu schneiden.

Dies sind die beiden Hauptcontainer, die ohne Anschluss eines ECS- Systems verfügbar sind. In einer fortgeschritteneren Version gibt es mehrere weitere Arten von Containern.

NativeList . Es ist eine regelmäßige Liste von Daten.
NativeHashMap . Ein Analogon eines Wörterbuchs mit einem Schlüssel und einem Wert.
NativeMultiHashMap . Dieselbe NativeHashMap mit nur wenigen Werten unter einem Schlüssel.
NativeQueue Liste der Datenwarteschlangen.

Da wir ohne Anschluss eines ECS- Systems arbeiten, stehen uns nur NativeArray und NativeSlice zur Verfügung.

Bevor Sie mit dem praktischen Teil fortfahren, müssen Sie den wichtigsten Punkt analysieren - die Erstellung von Instanzen.

Erstellen Sie Container


Wie bereits erwähnt, stellen diese Container eine Brücke dar, über die Daten zwischen Threads synchronisiert werden. Das Task-System öffnet diese Brücke vor Arbeitsbeginn und schließt sie nach Abschluss. Der Öffnungsprozess wird als " Zuweisung " ( Zuweisung ) oder "Zuweisung von Speicher" bezeichnet , der Schließvorgang als " Freigabe von Ressourcen " ( Entsorgen ).

Die Zuordnung bestimmt, wie lange die Aufgabe die Daten im Container verwenden kann - mit anderen Worten, wie lange die Brücke geöffnet sein wird.

Um diese beiden Prozesse besser zu verstehen, schauen wir uns das folgende Bild an.

Bild

Der untere Teil zeigt den Lebenszyklus des Hauptthreads ( Hauptthread ), der in der Anzahl der Frames berechnet wird. Im ersten Frame erstellen wir einen weiteren parallelen Thread ( Neuer Thread), der für eine bestimmte Anzahl von Frames vorhanden ist, und schließen dann sicher.
Im selben neuen Thread kommt die Aufgabe mit dem Container an.

Schauen Sie sich jetzt den oberen Rand des Bildes an.

Bild

Der weiße Balken Allocation zeigt die Lebensdauer des Containers an. Im ersten Frame wird der Container zugewiesen - die Brücke wird geöffnet, bis zu diesem Zeitpunkt der Container nicht vorhanden war. Nachdem alle Berechnungen in der Aufgabe abgeschlossen wurden, wird der Container aus dem Speicher freigegeben und im 9. Frame wird die Brücke geschlossen.

Auch auf diesem Streifen ( Zuordnung ) befinden sich Zeitsegmente ( Temp , TempJob und Presistent ). Jedes dieser Segmente zeigt die geschätzte Lebensdauer des Containers.

Warum werden diese Segmente benötigt? Tatsache ist, dass die Ausführung einer Aufgabe nach Dauer unterschiedlich sein kann, wir können sie direkt in derselben Methode ausführen, in der wir sie erstellt haben, oder wir können die Ausführungszeit der Aufgabe verlängern, wenn sie recht kompliziert ist, und diese Segmente zeigen, wie dringend und wie lange die Aufgabe die Daten verwenden kann im Behälter.

Wenn es immer noch nicht klar ist, werde ich jede Art der Zuordnung anhand eines Beispiels analysieren.

Jetzt können wir mit dem praktischen Teil des Erstellens von Containern fortfahren. Dazu kehren wir zur Start- Methode des TestJob- Skripts zurück und erstellen eine neue Instanz des NativeArray- Containers. Vergessen Sie nicht, die erforderlichen Bibliotheken zu verbinden.

Temp


Testjob
 using Unity.Jobs; using Unity.Collections; public class TestJob : MonoBehaviour { void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); } } 


Um eine neue Containerinstanz zu erstellen, müssen Sie die Größe und den Typ der Zuordnung in ihrem Konstruktor angeben. In diesem Beispiel wird der Temp- Typ verwendet, da die Aufgabe nur in der Start- Methode ausgeführt wird.

Initialisieren Sie nun genau dieselbe Array-Variable in der Struktur der SimpleJob- Task.

Simplejob
 public struct SimpleJob : IJob { public NativeArray<int> array; public void Execute() {} } 


Fertig. Jetzt können Sie die Aufgabe selbst erstellen und eine Array-Instanz an sie übergeben.

Starten Sie
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; } 


Um die Aufgabe dieses Mal auszuführen , verwenden wir das JobHandle- Handle, um sie durch Aufrufen derselben Schedule- Methode abzurufen .

Starten Sie
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); } 


Jetzt können Sie die Complete- Methode an ihrem Handle aufrufen und prüfen, ob die Aufgabe abgeschlossen ist, um den Text in der Konsole anzuzeigen.

Starten Sie
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print(" "); } 


Wenn Sie die Aufgabe in dieser Form ausführen, wird nach dem Start des Spiels ein fetter roter Fehler angezeigt, der besagt, dass Sie den Array-Container nach Abschluss der Aufgabe nicht aus den Ressourcen freigegeben haben.

So etwas in der Art.

Bild

Um dies zu vermeiden, rufen Sie nach Abschluss der Aufgabe die Dispose- Methode für den Container auf.

Starten Sie
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print("Complete"); array.Dispose(); } 


Dann können Sie es sicher neu starten.
Aber die Aufgabe macht nichts! - Fügen Sie dann einige Aktionen hinzu.

Simplejob
 public struct SimpleJob : IJob { public NativeArray<int> array; public void Execute() { for(int i = 0; i < array.Length; i++) { array[i] = i * i; } } } 


Bei der Execute- Methode multipliziere ich den Index jedes Elements des Arrays mit mir selbst und schreibe ihn zurück in das Array- Array , um das Ergebnis in der Start- Methode an die Konsole zu drucken.

Starten Sie
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print(job.array[job.array.Length - 1]); array.Dispose(); } 


Was ist das Ergebnis in der Konsole, wenn wir das letzte Element des Arrays im Quadrat drucken?

Auf diese Weise können Sie Container erstellen, in Aufgaben einfügen und Aktionen für sie ausführen.

Dies war ein Beispiel für die Verwendung des Temp- Zuordnungstyps, bei dem eine Aufgabe innerhalb eines Frames ausgeführt wird. Dieser Typ wird am besten verwendet, wenn Sie schnell Berechnungen durchführen müssen, ohne den Hauptthread zu laden. Sie müssen jedoch vorsichtig sein, wenn die Aufgabe zu kompliziert ist oder wenn viele davon auftreten, kann es zu einem Durchhängen kommen. In diesem Fall ist es besser, den TempJob- Typ zu verwenden , den ich später analysieren werde.

Tempjob


In diesem Beispiel werde ich die Struktur der SimpleJob- Task leicht ändern und von einer anderen IJobParallelFor- Schnittstelle erben.

Simplejob
 public struct SimpleJob : IJobParallelFor { public NativeArray<Vector2> array; public void Execute(int index) {} } 


Da die Aufgabe länger als ein Frame ausgeführt wird, werden die Ergebnisse der Aufgabe in verschiedenen Awake- und Start- Methoden ausgeführt und gesammelt , die in Form einer Coroutine dargestellt werden. Ändern Sie dazu das Erscheinungsbild der TestJob- Klasse ein wenig .

Testjob
 public class TestJob : MonoBehaviour { private NativeArray<Vector2> array; private JobHandle handle; void Awake() {} IEnumerator Start() {} } 


In der Awake- Methode erstellen wir eine Aufgabe und einen Container mit Vektoren und geben in der Start- Methode die empfangenen Daten aus und geben Ressourcen frei.

Wach auf
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; } 


Auch hier wird ein Array- Container mit der Art der Zuordnung TempJob erstellt. Anschließend erstellen wir eine Aufgabe und erhalten ihr Handle, indem wir die Schedule- Methode mit geringfügigen Änderungen aufrufen.

Wach auf
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; this.handle = job.Schedule(100, 5) } 


Der erste Parameter in der Schedule- Methode gibt an, wie oft die Aufgabe ausgeführt wird. Hier ist dieselbe Zahl wie die Größe des Array- Arrays .
Der zweite Parameter gibt an, wie viele Blöcke die Aufgabe gemeinsam nutzen sollen.

Welche anderen Blöcke?
Früher, um eine Aufgabe abzuschließen, hat ein Thread die Execute- Methode nur einmal aufgerufen. Jetzt muss diese Methode 100 Mal aufgerufen werden, sodass der Scheduler diese 100-maligen Wiederholungen in Blöcke aufteilt, die er zwischen den Threads verteilt, um keinen separaten Thread zu laden. In diesem Beispiel werden hundert Wiederholungen in 5 Blöcke mit jeweils 20 Wiederholungen unterteilt, dh der Scheduler verteilt diese 5 Blöcke vermutlich auf 5 Threads, wobei jeder Thread die Execute- Methode 20 Mal aufruft . In der Praxis ist es natürlich keine Tatsache, dass der Scheduler genau das tut, es hängt alles von der Arbeitslast des Systems ab, sodass möglicherweise alle 100 Wiederholungen in einem Thread stattfinden.

Jetzt können Sie die Complete- Methode im Task-Handle aufrufen.

Wach auf
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; this.handle = job.Schedule(100, 5); this.handle.Complete(); } 


In der Start- Coroutine überprüfen wir die Ausführung der Aufgabe und bereinigen dann den Container.

Starten Sie
 IEnumerator Start() { while(this.handle.isCompleted == false){ yield return new WaitForEndOfFrame(); } this.array.Dispose(); } 


Fahren wir nun mit den Aktionen in der Aufgabe selbst fort.

Simplejob
 public struct SimpleJob : IJobParallelFor { public NativeArray<Vector2> array; public void Execute(int index) { float x = index; float y = index; Vector2 vector = new Vector2(x * x, y * y / (y * 2)); this.array[index] = vector; } } 


Zeigen Sie nach Abschluss der Aufgabe in der Start- Methode alle Elemente des Arrays in der Konsole an.

Starten Sie
 IEnumerator Start() { while(this.handle.IsCompleted == false){ yield return new WaitForEndOfFrame(); } foreach(Vector2 vector in this.array) { print(vector); } this.array.Dispose(); } 


Wenn Sie fertig sind, können Sie das Ergebnis anzeigen.

Schauen Sie sich die folgenden Bilder an , um den Unterschied zwischen IJob und IJobParallelFor zu verstehen.
In IJob können Sie beispielsweise eine einfache for- Schleife verwenden, um Berechnungen mehrmals durchzuführen. In jedem Fall kann ein Thread die Execute- Methode jedoch nur einmal für die gesamte Dauer der Aufgabe aufrufen. Auf diese Weise kann eine Person Hunderte derselben Aktionen hintereinander ausführen.

Bild

IJobParallelFor bietet nicht nur die Möglichkeit , eine Aufgabe in einem Thread mehrmals auszuführen, sondern diese Wiederholungen auch auf andere Threads zu verteilen.

Bild

Im Allgemeinen ist die Art der Zuordnung TempJob perfekt für die meisten Aufgaben, die über mehrere Frames ausgeführt werden.

Was aber, wenn Sie Daten auch nach Abschluss einer Aufgabe speichern müssen, was ist, wenn Sie sie nach Erhalt des Ergebnisses nicht sofort zerstören müssen? Hierzu ist es erforderlich, die Art der Zuordnung Persistent zu verwenden , was die Freigabe von Ressourcen dann „ bei Bedarf!“ Impliziert. .

Hartnäckig


Kehren wir zur TestJob- Klasse zurück und ändern sie. Jetzt erstellen wir Aufgaben in der OnEnable- Methode, überprüfen deren Ausführung in der Update- Methode und bereinigen Ressourcen in der OnDisable- Methode.
In diesem Beispiel verschieben wir das Objekt in der Update- Methode. Zur Berechnung der Trajektorie verwenden wir zwei Vektorcontainer - inputArray, in die wir die aktuelle Position und outputArray einfügen, von wo aus wir die Ergebnisse erhalten.

Testjob
 public class TestJob : MonoBehaviour { private NativeArray<Vector2> inputArray; private NativeArray<Vector2> outputArray; private JobHandle handle; void OnEnable() {} void Update() {} void OnDisable() {} } 


Wir werden auch die Struktur der SimpleJob- Task leicht ändern, indem wir sie von der IJob- Schnittstelle erben, um sie einmal auszuführen.

Simplejob
 public struct SimpleJob : IJob { public void Execute() {} } 


In der Aufgabe selbst werden wir auch zwei Vektorcontainer verraten, einen Positionsvektor und ein numerisches Delta, die das Objekt zum Ziel bewegen.

Simplejob
 public struct SimpleJob : IJob { [ReadOnly] public NativeArray<Vector2> inputArray; [WriteOnly] public NativeArray<Vector2> outputArray; public Vector2 position; public float delta; public void Execute() {} } 


Die Attribute ReadOnly und WriteOnly zeigen die Flussbeschränkungen für die Aktionen an, die den Daten in den Containern zugeordnet sind. ReadOnly bietet den Stream nur zum Lesen von Daten aus dem Container an. Das WriteOnly- Attribut hingegen ermöglicht es dem Stream, nur Daten in den Container zu schreiben. Wenn Sie diese beiden Aktionen gleichzeitig mit einem Container ausführen müssen, müssen Sie ihn überhaupt nicht mit einem Attribut markieren.

Fahren wir mit der OnEnable- Methode der TestJob- Klasse fort, in der die Container initialisiert werden.

Onenable
 void OnEnable() { this.inputArray = new NativeArray<Vector2>(1, Allocator.Persistent); this.outputArray = new NativeArray<Vector2>(1, Allocator.Persistent); } 


Die Abmessungen der Container sind einfach, da Parameter nur einmal gesendet und empfangen werden müssen. Die Art der Zuordnung ist dauerhaft .
Bei der OnDisable- Methode geben wir die Ressourcen der Container frei.

Ondisable
 void OnDisable() { this.inputArray.Dispose(); this.outputArray.Dispose(); } 


Erstellen wir eine separate CreateJob- Methode, in der wir eine Aufgabe mit ihrem Handle erstellen und dort mit Daten füllen.

CreateJob
 void CreateJob() { SimpleJob job = new SimpleJob(); job.delta = Time.deltaTime; Vector2 position = this.transform.position; job.position = position; Vector2 newPosition = position + Vector2.right; this.inputArray[0] = newPosition; job.inputArray = this.inputArray; job.outputArray = this.outputArray; this.handle = job.Schedule(); this.handle.Complete(); } 


Eigentlich wird inputArray hier nicht wirklich benötigt, da es möglich ist, einen Richtungsvektor nur auf die Aufgabe zu übertragen, aber ich denke, es ist besser zu verstehen, warum diese ReadOnly- und WriteOnly- Attribute überhaupt benötigt werden.

In der Update- Methode prüfen wir, ob die Aufgabe abgeschlossen ist. Anschließend wenden wir das erhaltene Ergebnis auf die Objekttransformation an und führen es erneut aus.

Update
 void Update() { if (this.handle.IsCompleted) { Vector2 newPosition = this.outputArray[0]; this.transform.position = newPosition; CreateJob(); } } 


Bevor wir beginnen, werden wir die OnEnable- Methode leicht anpassen , sodass die Aufgabe unmittelbar nach der Initialisierung der Container erstellt wird.

Onenable
 void OnEnable() { this.inputArray = new NativeArray<Vector2>(1, Allocator.Persistent); this.outputArray = new NativeArray<Vector2>(1, Allocator.Persistent); CreateJob(); } 


Fertig, jetzt können Sie zur Aufgabe selbst gehen und die erforderlichen Berechnungen in der Execute- Methode durchführen.

Ausführen
 public void Execute() { Vector2 newPosition = this.inputArray[0]; newPosition = Vector2.Lerp(this.position, newPosition, this.delta); this.outputArray[0] = newPosition; } 


Um das Ergebnis der Arbeit zu sehen, können Sie das TestJob- Skript auf ein Objekt werfen und das Spiel ausführen.

Zum Beispiel verschiebt sich mein Sprite nur allmählich nach rechts.

Animation
Bild

Im Allgemeinen eignet sich die Art der Zuordnung Persistent hervorragend für wiederverwendbare Container, die nicht jedes Mal zerstört und neu erstellt werden müssen.

Also, welche Art zu verwenden !?
Der Temp- Typ eignet sich am besten für die schnelle Durchführung von Berechnungen. Wenn die Aufgabe jedoch zu komplex und zu groß ist, kann es zu einem Durchhang kommen.
Der TempJob- Typ eignet sich hervorragend für die Arbeit mit Unity- Objekten, sodass Sie die Parameter von Objekten ändern und sie beispielsweise im nächsten Frame anwenden können.
Der Typ Persistent kann verwendet werden, wenn die Geschwindigkeit für Sie nicht wichtig ist, Sie jedoch nur ständig Daten nebenbei berechnen müssen, z. B. Daten über ein Netzwerk verarbeiten oder die Arbeit einer KI.

Ungültig und keine
Es gibt zwei weitere Arten der Zuweisung: Ungültig und Keine . Sie werden jedoch häufiger zum Debuggen benötigt und nehmen nicht an der Arbeit teil.


Jobhandle


Unabhängig davon lohnt es sich, die Funktionen des Aufgabenhandles zu analysieren, da dieses kleine Handle nicht nur den Prozess der Aufgabenausführung überprüft, sondern auch ganze Netzwerke von Aufgaben durch Abhängigkeiten erstellen kann (obwohl ich sie lieber eher als Warteschlangen bezeichne).

Wenn Sie beispielsweise zwei Aufgaben in einer bestimmten Reihenfolge ausführen müssen, müssen Sie dazu nur das Handle einer Aufgabe an das Handle einer anderen anhängen.

Es sieht ungefähr so ​​aus.

Bild

Jedes einzelne Handle enthält zunächst eine eigene Aufgabe. In Kombination erhalten wir jedoch ein neues Handle mit zwei Aufgaben.

Starten Sie
 void Start() { Job jobA = new Job(); JobHandle handleA = jobA.Schedule(); Job jobB = new Job(); JobHandle handleB = jobB.Schedule(); JobHandle result = JobHandle.CombineDependecies(handleA, handleB); result.Complete(); } 


Oder so.

Starten Sie
 void Start() { JobHandle handle; for(int i = 0; i < 10; i++) { Job job = new Job(); handle = job.Schedule(handle); } handle.Complete(); } 


Die Ausführungssequenz wird gespeichert und der Scheduler startet die nächste Aufgabe erst, wenn er von der vorherigen überzeugt ist. Beachten Sie jedoch, dass die Handle- Eigenschaft IsCompleted auf den Abschluss aller darin enthaltenen Aufgaben wartet.

Fazit


Container


  1. Vergessen Sie beim Arbeiten mit Daten in Containern nicht, dass es sich um Strukturen handelt. Wenn Sie also Daten im Container überschreiben, werden diese nicht geändert, sondern erneut erstellt.
  2. Was passiert, wenn Sie die Art der Zuordnung Temp festlegen und die Ressourcen nach Abschluss der Aufgabe nicht löschen? Der Fehler.
  3. Kann ich meine eigenen Container erstellen? Es ist möglich, dass die Unites den Prozess der Erstellung benutzerdefinierter Container hier ausführlich beschrieben haben, aber es ist besser, ein paar Mal darüber nachzudenken: Lohnt es sich, vielleicht gibt es genug normale Container!

Sicherheit!


Statische Daten.

Versuchen Sie nicht, statische Daten in einer Aufgabe zu verwenden ( zufällig und andere). Jeder Zugriff auf statische Daten verletzt die Sicherheit des Systems. Momentan können Sie auf statische Daten zugreifen, aber nur, wenn Sie sicher sind, dass sie sich während der Arbeit nicht ändern - das heißt, sie sind vollständig statisch und schreibgeschützt.

Wann soll das Task-System verwendet werden?

Alle diese Beispiele, die hier im Artikel aufgeführt sind, sind nur bedingt und zeigen, wie Sie mit diesem System arbeiten und nicht wann Sie es verwenden. Das Task-System kann ohne ECS verwendet werden .Sie müssen verstehen, dass das System auch bei der Arbeit Ressourcen verbraucht und dass es aus jedem Grund einfach sinnlos ist, sofort Aufgaben zu schreiben und Containerhaufen zu erstellen - alles wird noch schlimmer. Zum Beispiel ist die Neuberechnung eines Arrays mit einer Größe von 10 Tausend Elementen nicht korrekt. Es dauert länger, bis der Scheduler funktioniert. Wenn Sie jedoch alle Polygone eines riesigen Terrans neu berechnen oder sogar generieren, ist dies die richtige Lösung. Sie können den Terran in Aufgaben aufteilen und jedes in einem separaten Stream verarbeiten.

Wenn Sie ständig in komplexe Berechnungen in Projekten involviert sind und ständig nach neuen Möglichkeiten suchen, um diesen Prozess weniger ressourcenintensiv zu gestalten, dann ist Job System genau das Richtige für Sie . , ECS . WebGL , Job System , , .

Source: https://habr.com/ru/post/de420829/


All Articles