Wie friere ich den Haupt-Thread in Unity nicht ein?

33

Ich habe einen Algorithmus zur Pegelerzeugung, der rechenintensiv ist. Wenn Sie es aufrufen, friert der Spielbildschirm immer ein. Wie kann ich die Funktion in einem zweiten Thread platzieren, während das Spiel weiterhin einen Ladebildschirm anzeigt, um anzuzeigen, dass das Spiel nicht eingefroren ist?

DarkDestry
quelle
1
Sie können das Threading-System verwenden, um andere Jobs im Hintergrund auszuführen. Möglicherweise rufen sie jedoch nichts in der Unity-API auf, da diese Informationen nicht threadsicher sind. Nur zu kommentieren, weil ich das noch nicht getan habe und Ihnen keinen sicheren Beispielcode geben kann.
Almo
Verwendung Listvon Aktionen zum Speichern der Funktionen, die Sie im Haupt-Thread aufrufen möchten. sperren und kopieren Sie die ActionListe in der UpdateFunktion in eine temporäre Liste, löschen Sie die ursprüngliche Liste und führen Sie den ActionCode in dem Listauf dem Haupt-Thread aus. Siehe dazu UnityThread aus meinem anderen Beitrag . Zum Beispiel, um eine Funktion im Haupt-Thread aufzurufen, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programmer

Antworten:

48

Update: Unity führt 2018 ein C # -Jobsystem ein, um die Arbeit auszulagern und mehrere CPU-Kerne zu nutzen.

Die folgende Antwort geht diesem System voraus. Es wird immer noch funktionieren, aber in modernen Unity gibt es je nach Ihren Anforderungen möglicherweise bessere Optionen. Insbesondere scheint das Auftragssystem einige der Einschränkungen zu beseitigen, auf die manuell erstellte Threads sicher zugreifen können (siehe unten). Entwickler, die mit dem Vorschaubericht experimentieren, führen beispielsweise Raycasts durch und konstruieren parallel Netze .

Ich möchte Benutzer mit Erfahrung in der Verwendung dieses Job-Systems einladen, ihre eigenen Antworten hinzuzufügen, die den aktuellen Status der Engine widerspiegeln.


Ich habe in der Vergangenheit Threading für Schwergewichtsaufgaben in Unity verwendet (normalerweise Bild- und Geometrieverarbeitung), und es unterscheidet sich nicht wesentlich von der Verwendung von Threads in anderen C # -Anwendungen, mit zwei Einschränkungen:

  1. Da Unity eine etwas ältere Teilmenge von .NET verwendet, gibt es einige neuere Threading-Funktionen und -Bibliotheken, die wir nicht sofort verwenden können, aber die Grundlagen sind alle vorhanden.

  2. Wie Almo in einem Kommentar oben bemerkt, sind viele Unity-Typen nicht threadsicher und lösen Ausnahmen aus, wenn Sie versuchen, sie außerhalb des Hauptthreads zu konstruieren, zu verwenden oder sogar zu vergleichen. Dinge zu beachten:

    • Ein häufiger Fall ist die Überprüfung, ob eine GameObject- oder Monobehaviour-Referenz null ist, bevor versucht wird, auf ihre Mitglieder zuzugreifen. myUnityObject == nullruft einen überladenen Operator für alles auf, was von UnityEngine.Object abstammt, System.Object.ReferenceEquals()funktioniert jedoch bis zu einem gewissen Grad. Denken Sie daran, dass ein Destroy () ed-GameObject mit der Überladung gleich null ist, aber noch nicht ReferenceEqual to null ist.

    • Das Lesen von Parametern aus Unity-Typen ist normalerweise in einem anderen Thread sicher (insofern wird nicht sofort eine Ausnahme ausgelöst, solange Sie darauf achten, wie oben beschrieben auf Nullen zu prüfen). Beachten Sie jedoch die Warnung von Philipp, dass der Haupt-Thread möglicherweise den Status ändert während du es liest. Sie müssen sich darüber im Klaren sein, wer was und wann ändern darf, um zu vermeiden, dass ein inkonsistenter Status gelesen wird. Dies kann zu Fehlern führen, die sich nur schwer aufspüren lassen, da sie von Zeitabständen im Millisekundenbereich zwischen den Threads abhängen, die wir können nicht nach Belieben reproduzieren.

    • Zufällige und zeitstatische Elemente sind nicht verfügbar. Erstellen Sie eine Instanz von System.Random pro Thread, wenn Sie Zufälligkeiten benötigen, und System.Diagnostics.Stopwatch, wenn Sie Zeitinformationen benötigen.

    • Mathf-Funktionen, Vektor-, Matrix-, Quaternions- und Farbstrukturen funktionieren alle innerhalb von Threads einwandfrei, sodass Sie die meisten Berechnungen separat ausführen können

    • Das Erstellen von GameObjects, das Anhängen von Monobehaviours oder das Erstellen / Aktualisieren von Texturen, Meshes, Materialien usw. muss im Haupt-Thread erfolgen. In der Vergangenheit, als ich damit arbeiten musste, habe ich eine Produzenten-Konsumenten-Warteschlange eingerichtet, in der mein Arbeitsthread die Rohdaten aufbereitet (wie eine große Anzahl von Vektoren / Farben, die auf ein Netz oder eine Textur angewendet werden sollen). und ein Update oder eine Coroutine im Haupt-Thread fragt nach Daten und wendet diese an.

Mit diesen Notizen aus dem Weg, hier ist ein Muster, das ich oft für Zwirnarbeiten verwende. Ich kann nicht garantieren, dass es sich um einen Best-Practice-Stil handelt, aber er erledigt die Arbeit. (Kommentare oder Änderungen zur Verbesserung sind willkommen - ich weiß, dass Threading ein sehr tiefes Thema ist, von dem ich nur die Grundlagen kenne.)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

Wenn Sie die Arbeit aus Geschwindigkeitsgründen nicht unbedingt auf mehrere Threads aufteilen müssen und nur nach einer Möglichkeit suchen, sie blockierungsfrei zu machen, damit der Rest Ihres Spiels weiter läuft, ist Coroutines eine leichtere Lösung . Dies sind Funktionen, die einige Arbeit erledigen können, um dann die Steuerung an den Motor zurückzugeben, um fortzufahren, was er tut, und zu einem späteren Zeitpunkt nahtlos fortzufahren.

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

Dies erfordert keine besonderen Reinigungsüberlegungen, da der Motor (soweit ich das beurteilen kann) Coroutinen von zerstörten Objekten für Sie entfernt.

Der gesamte lokale Status der Methode bleibt erhalten, wenn sie ausgegeben und fortgesetzt wird. In vielen Fällen ist es also so, als würde sie auf einem anderen Thread ohne Unterbrechung ausgeführt (Sie verfügen jedoch über alle Annehmlichkeiten, um auf dem Hauptthread ausgeführt zu werden). Sie müssen nur sicherstellen, dass jede Iteration kurz genug ist, damit Ihr Haupt-Thread nicht unangemessen verlangsamt wird.

Indem Sie sicherstellen, dass wichtige Vorgänge nicht durch einen Ertrag getrennt sind, können Sie die Konsistenz des Singlethread-Verhaltens erzielen - mit dem Wissen, dass kein anderes Skript oder System im Hauptthread Daten ändern kann, an denen Sie gerade arbeiten.

Die Yield Return-Linie bietet Ihnen einige Möglichkeiten. Sie können...

  • yield return null wird nach dem Update des nächsten Frames fortgesetzt ()
  • yield return new WaitForFixedUpdate() wird nach dem nächsten FixedUpdate () fortgesetzt
  • yield return new WaitForSeconds(delay) nach Ablauf einer bestimmten Spielzeit wieder aufzunehmen
  • yield return new WaitForEndOfFrame() wird fortgesetzt, nachdem die GUI das Rendern beendet hat
  • yield return myRequestWo myRequestist eine WWW- Instanz, um fortzufahren, sobald die angeforderten Daten aus dem Web oder von der CD geladen wurden?
  • yield return otherCoroutineWo otherCoroutineist eine Coroutine-Instanz , die nach otherCoroutineAbschluss fortgesetzt werden soll ? Dies wird häufig in der Form verwendet, yield return StartCoroutine(OtherCoroutineMethod())um die Ausführung an eine neue Koroutine zu ketten, die selbst nachgeben kann, wenn sie dies möchte.

    • Experimentell kann durch Überspringen der zweiten StartCoroutineund einfaches Schreiben yield return OtherCoroutineMethod()das gleiche Ziel erreicht werden, wenn Sie die Ausführung im gleichen Kontext verketten möchten.

      Das Einschließen in ein StartCoroutinekann dennoch nützlich sein, wenn Sie die verschachtelte Coroutine in Verbindung mit einem zweiten Objekt ausführen möchten, zyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... je nachdem, wann die Coroutine ihre nächste Runde machen soll.

Oder yield break;um die Coroutine zu stoppen, bevor sie das Ende erreicht, wie Sie es return;von einer herkömmlichen Methode gewohnt sind.

DMGregory
quelle
Gibt es eine Möglichkeit, Coroutinen zu verwenden, um erst zu ergeben, nachdem die Iteration mehr als 20 ms dauert?
DarkDestry
@DarkDestry Sie können eine Stoppuhr-Instanz verwenden, um die Zeit zu bestimmen, die Sie in Ihrer inneren Schleife verbringen, und in eine äußere Schleife ausbrechen, in der Sie nachgeben, bevor Sie die Stoppuhr zurücksetzen und die innere Schleife fortsetzen.
DMGregory
1
Gute Antwort! Ich möchte nur hinzufügen, dass ein weiterer Grund für die Verwendung von Coroutinen darin besteht, dass wenn Sie jemals für Webgl bauen müssen, dies funktioniert, was nicht der Fall ist, wenn Sie Threads verwenden. Sitzen mit diesen Kopfschmerzen in einem großen Projekt
Mikael Högström
Gute Antwort und danke. Ich frage mich nur, was
passiert
@flankechen Das Starten und Herunterfahren von Threads sind relativ kostspielige Vorgänge. Daher ziehen wir es oft vor, den Thread verfügbar zu halten, aber im Ruhezustand zu bleiben. Verwenden Sie beispielsweise Semaphore oder Monitore, um zu signalisieren, wann wir neue Arbeit haben. Möchten Sie eine neue Frage veröffentlichen, in der Ihr Anwendungsfall detailliert beschrieben wird, und Leute können effiziente Wege vorschlagen, dies zu erreichen?
DMGregory
0

Sie können Ihre schwere Berechnung in einen anderen Thread stellen, aber die API von Unity ist nicht threadsicher. Sie müssen sie im Haupt-Thread ausführen.

Sie können dieses Paket auch im Asset Store testen, um die Verwendung von Threading zu vereinfachen. http://u3d.as/wQg Sie können einfach nur eine Codezeile verwenden, um einen Thread zu starten und die Unity-API sicher auszuführen.

Qi Haoyan
quelle
0

@DMGregory hat es wirklich gut erklärt.

Sowohl Threading als auch Coroutinen können verwendet werden. Früher, um den Hauptthread auszulagern und später die Kontrolle an den Hauptthread zurückzugeben. Schweres Heben zum Trennen des Fadens ist sinnvoller. Daher könnten Warteschlangen das sein, wonach Sie suchen.

Es gibt ein wirklich gutes JobQueue- Beispielskript im Unity Wiki.

IndieForger
quelle