Zum Inhalt springen
Unity Async Asynchrones Tutorial, was unterscheidet es eigentlich vom nativen .Net?

Unity Async Asynchrones Tutorial, was unterscheidet es eigentlich vom nativen .Net?

2024-12-05 19:55

Was Async/Await ist, wird hier nicht erklärt, es ist ähnlich wie Coroutines, aber prägnanter. Hier wird vorausgesetzt, dass Sie über Kenntnisse der modernen asynchronen Programmierung verfügen.

Fassen wir die interne Implementierung von Unity zusammen, um klar zu verstehen, was eigentlich passiert. Einfach gesagt, hat Unity intern einige Anpassungen vorgenommen, um zwei Punkte sicherzustellen, wenn der Aufrufer der Hauptthread ist: 1. Der Aufrufer wechselt nicht den Thread, 2. Die aufgerufene asynchrone Funktion wird nicht in einem anderen Thread ausgeführt.

Ziele von Unity für den Async-Modus

Der Async-Modus ist moderner und die zukünftige Richtung, aber Unity plant derzeit nicht, Async anstelle von Coroutines (IEnumerators) zu verwenden. Das Hauptziel ist der Einsatz bei IO-Operationen und ähnlichem.

Unity Async hat eine höhere Leistung als Coroutines, aber eine geringere Parallelität

Geeignet für die Verwendung durch Singleton-/Hauptklassenobjekte, wie Kamera, UI, IO-Methoden usw. Bei einer geringen Anzahl ist die Leistung von Async höher als bei Coroutines. Für Code, bei dem eine große Anzahl von Objekten parallel laufen muss, sollten weiterhin Coroutines verwendet werden.

Leistungsaspekte

Geben Sie nach Möglichkeit das von Unity gekapselte Awaitable zurück, nicht Task (Unity 6)

.Net verwendet Task für Async, wie folgt:

  async Task function() {
        await Task.Delay(1000);
  }

Die native .Net Task erstellt bei jeder Rückgabe ein neues Objekt, während das von Unity gekapselte Awaitable-Objekt gemeinsam genutzt wird, daher ist der GC-Druck geringer. Awaitable hat außerdem den Vorteil des “kostenlosen” (verlustfreien) Thread-Wechsels, siehe nächster Abschnitt.

  async Awaitable function() {
        while(true) {
            await Awaitable.NextFrameAsync();
        }
  }

Für Methoden, die wiederholt await benötigen, ähnlich dem obigen Beispielcode, verwenden Sie bitte den Rückgabewert Awaitable. Natürlich spielt es bei einmaligen await-Methoden wie IO keine Rolle.

Da Awaitable-Objekte gemeinsam genutzt werden (nach await werden sie von anderen verwendet), dürfen sie nicht wiederholt geawaited werden (wie im folgenden Code), da dies zu einer Deadlock-Situation führen würde.

async Awaitable function() {
        while(true) {
            var returned_awaitable = Awaitable.NextFrameAsync();
            await returned_awaitable;
            await returned_awaitable; //Deadlock
        }
  }

Unity verwaltet den Async-Kontext (Wechsel) intern anders als .Net

Das Hauptkonzept von Async, das Wiederaufnehmen (Resume), bezieht sich auf das Zurückschalten und die Fortsetzung der Ausführung des Codes nach dem await.

Der “Kontext” bestimmt, wie der await-Code wieder aufgenommen wird. .Net verwendet den Thread-Pool, um zwischen verschiedenen asynchronen Codes zu wechseln. Zum Beispiel der folgende .Net-Code:

using System;

async Task function() {
    Console.WriteLine($"task start: Tid {Environment.CurrentManagedThreadId}");
    await Task.Delay(1000);
    Console.WriteLine($"task end: Tid {Environment.CurrentManagedThreadId}");
}

Console.WriteLine($"caller start, Tid{Environment.CurrentManagedThreadId}");
await function();
Console.WriteLine($"caller end, Tid{Environment.CurrentManagedThreadId}");

Das Ausführungsergebnis ist:

caller start, Tid12
task start: Tid 12
task end: Tid 38
caller end, Tid38

Man kann sehen, dass sowohl beim Aufrufer als auch beim Ausführenden ein Thread-Wechsel stattgefunden hat.

Um sicherzustellen, dass vom Hauptthread aufgerufene Methoden nur im Hauptthread ausgeführt werden, hat Unity selbst eine Kontextmethode namens UnitySynchronizationContext gekapselt (dies ist eine interne Implementierung, wir müssen uns nicht darum kümmern, es reicht zu wissen).

Machen Sie sich also keine Sorgen um Thread-Probleme. Egal, ob der Rückgabewert Awaitable oder .Nets Task ist, solange er vom Hauptthread aufgerufen wird, wird er von UnitySynchronizationContext verwaltet und die Ausführung im Hauptthread sichergestellt.

Allerdings kann eine Async-Funktion mit Task-Rückgabewert beim Wiederaufnehmen (Resume) erst im nächsten Frame und nicht sofort wieder aufgenommen werden. Außerdem muss Unity einen zusätzlichen Schritt ausführen, um den .Net-Kontext zu ersetzen, daher ist Awaitable leistungsfähiger.

Natürlich haben Async-Funktionen, die von anderen Threads aufgerufen werden und keine Hauptthread-Einschränkung benötigen, weiterhin den .Net-Thread-Pool.

Verwendungsmuster

Fire-and-Forget-Muster

Beim Aufruf einer Async-Funktion kann await weggelassen werden. Das System führt die Async-Funktion intern weiter aus.

async Awaitable worker() {
    await Awaitable.WaitForSecondsAsync(5);
    Debug.Log($"worker end, frame {Time.frameCount}");
}

void caller() {
    _ = worker();
    Debug.Log($"caller end, frame {Time.frameCount}");
}

Ergebnis:

caller end, frame 1
worker end, frame 292

caller hat kein await geschrieben, aber worker wird trotzdem ausgeführt, nur wird nicht mehr auf seine Beendigung gewartet.

In .Net würde worker weiterhin im Thread-Pool ausgeführt werden, in Unity hingegen wird er weiterhin durch die Hauptthread-Schleife ausgeführt.

Dieses Muster wird auch häufig verwendet, um in einer normalen synchronen (Sync) Funktion eine asynchrone (Async) Funktion aufzurufen.

Hinweis: Diese Methode entspricht async void, wird auf oberster Ebene ausgeführt (Stack geht verloren), Ausnahmen im Code sind nicht sichtbar und verhalten sich, als ob der Code nicht ausgeführt wurde. Dies bedeutet nicht, dass das Muster unbrauchbar ist.

Mit Async kann Unity Multithread-Programmierung sehr einfach umsetzen

Damit kann Multithread-Code einfach implementiert werden, ohne Jobs o.ä. zu benötigen. Beliebige Codeabschnitte können in verschiedenen Threads ausgeführt werden.

Wenn Sie zum Hintergrund-Thread-Pool (ähnlich der .Net-Thread-Pool-Implementierung) oder zum Hauptthread wechseln möchten, können Sie dies manuell angeben. Zum Beispiel:

async Awaitable function() {
    await Awaitable.BackgroundThreadAsync();
    ...
    return null;
}

Der Code nach diesem await bis zum Ende der Funktion wird im Hintergrund-.Net-Thread-Pool ausgeführt.

Wenn der Aufrufer der Hauptthread ist, ändert der Wechsel nicht den Thread des Aufrufers, was sehr praktisch ist. Beachten Sie, dass der Wechsel mindestens 1 Frame Zeit benötigt.

Der Wechsel zum Hauptthread erfolgt mit await Awaitable.MainThreadAsync();, normalerweise muss dies nicht aufgerufen werden.

Leider können Sie unter WebGL (wasm) .Net-Multithreading nicht verwenden, einschließlich aller Task-Methoden und Awaitable.BackgroundThreadAsync().

Async-Funktionen können als void definiert werden, was für verschiedene event-Callbacks praktisch ist

Wenn Sie möchten, dass ein Callback Ihre Async-Funktion ausführt, müssen Sie nicht mehr eine normale Funktion um Ihre asynchrone Funktion wickeln. Sie können async void verwenden, ähnlich wie folgt:

async void function(CallbackContext ctx) {
    try{
        ...etwas tun...
        await Awaitable.WaitForSecondsAsync(5);
    }catch (Exception e){
        Debug.LogError(e);
    }
}

void Start() {
    InputAction.performed += function;
}

Beachten Sie, dass async void auf oberster Ebene ausgeführt wird, daher werden Ausnahmen in der Funktion nicht extern erkannt. Sie müssen sie selbst mit catch behandeln.

Asynchrone Form von MonoBehaviour-Ereignissen

Start, Update usw. von MonoBehaviour können im Async-Modus verwendet werden, z.B.: async Awaitable Start() {}. Denken Sie jedoch daran, dass der asynchrone Start oder Awake aufgerufen wird, bevor er abgeschlossen ist, Update aufgerufen wird.

Zuletzt bearbeitet
hugo-builder
hugo-builder · · 自动翻译 2024-12-05... · 55a10c6
Weitere Mitwirkende
...