Aller au contenu
Unity Async Asynchrone Tutoriel : En quoi est-ce différent du natif .Net ?

Unity Async Asynchrone Tutoriel : En quoi est-ce différent du natif .Net ?

2024-12-05 19:55

Pas besoin de présenter Async/Await, c’est similaire aux coroutines mais plus concis. On suppose ici que vous avez des connaissances en programmation asynchrone moderne.

Résumons l’implémentation interne d’Unity pour comprendre ce qui se passe. En bref, Unity effectue certains traitements internes pour garantir deux points lorsque l’appelant est le thread principal : 1. L’appelant ne subira pas de changement de thread, 2. La fonction asynchrone appelée ne s’exécutera pas sur un autre thread.

L’objectif du modèle Async pour Unity

Le modèle Async est plus moderne et représente l’avenir, mais Unity ne prévoit actuellement pas de remplacer les coroutines (IEnumerators) par Async. Son objectif principal est de l’utiliser pour des opérations de type IO, etc.

Unity Async est plus performant que les coroutines, mais avec un niveau de parallélisme plus faible

Il convient à des objets de type singleton/entité principale, comme la caméra, l’interface, les méthodes IO, etc. Pour un petit nombre d’objets, Async est plus performant que les coroutines. Pour du code où un grand nombre d’Objects doivent s’exécuter en parallèle, il faut toujours utiliser des coroutines.

Partie Performance

Privilégiez le retour de l’Awaitable encapsulé par Unity plutôt que de Task (Unity 6)

Le Async de .Net utilise Task, comme ci-dessous :

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

Le Task natif de .Net crée un nouvel objet à chaque retour, tandis que l’objet Awaitable encapsulé par Unity est partagé, ce qui réduit la pression sur le GC. Awaitable présente également l’avantage d’un changement de thread “gratuit” (sans coût), voir la section suivante.

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

Pour les méthodes nécessitant des await répétés, comme l’exemple de code ci-dessus, utilisez une valeur de retour Awaitable. Bien sûr, pour les méthodes avec un await unique comme les IO, cela n’a pas d’importance.

Puisque l’objet Awaitable est partagé (il est utilisé par d’autres après un await), il ne peut pas être awaité plusieurs fois (comme dans le code ci-dessous), cela provoquerait un interblocage.

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

Unity gère le contexte (changement) d’Async en interne, différemment de .Net

Le concept principal d’Async, la reprise (Resume), fait référence au retour et à la poursuite de l’exécution du code après un await.

Le “contexte” détermine comment reprendre le code après un await. .Net utilise le pool de threads pour basculer entre différents codes asynchrones. Par exemple, le code .Net suivant :

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}");

Donne le résultat :

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

On peut voir que l’appelant et l’exécutant ont tous deux changé de thread.

Pour garantir que les méthodes appelées depuis le thread principal s’exécutent uniquement sur le thread principal, Unity a encapsulé sa propre méthode de contexte appelée UnitySynchronizationContext (c’est une implémentation interne, il suffit de savoir qu’elle existe).

Ainsi, pas de souci de thread : que la valeur de retour soit Awaitable ou le Task de .Net, tant que l’appel est effectué depuis le thread principal, il sera géré par UnitySynchronizationContext et garanti de s’exécuter sur le thread principal.

Cependant, pour les fonctions Async retournant un Task, la reprise (Resume) ne peut se faire qu’à la frame suivante et non immédiatement. De plus, Unity doit effectuer une étape supplémentaire pour remplacer le contexte .Net, ce qui rend Awaitable plus performant.

Bien sûr, les fonctions Async appelées depuis d’autres threads, sans besoin de restriction du thread principal, continueront d’utiliser le pool de threads .Net.

Modèles d’utilisation

Mode “Fire and Forget”

Lors de l’appel d’une fonction Async, on peut ne pas utiliser await. Le système exécutera alors cette fonction Async en continu en interne.

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

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

Résultat :

caller end, frame 1
worker end, frame 292

caller n’a pas écrit await, mais worker s’exécutera tout de même, simplement sans l’attendre.

Dans .Net, worker s’exécuterait en continu dans le pool de threads. Dans Unity, il est exécuté en continu par la boucle du thread principal.

Ce modèle est également couramment utilisé pour appeler une fonction Async depuis une fonction Sync (synchrone) ordinaire.

Remarque : Cette méthode équivaut à async void, elle s’exécute au plus haut niveau (pile d’appels perdue). Les exceptions dans le code seront invisibles, donnant l’impression que le code ne s’est pas exécuté. Ce n’est pas que ce modèle soit inutilisable.

Avec Async, Unity permet une programmation multithread très pratique

Cela permet de facilement implémenter du code multithread sans avoir besoin de Jobs, etc. N’importe quel fragment de code peut s’exécuter sur un thread différent.

Si vous souhaitez basculer vers le pool de threads d’arrière-plan (similaire à l’implémentation du pool de threads .Net), ou vers le thread principal, vous pouvez le spécifier manuellement. Par exemple :

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

Le code après cette ligne await, jusqu’à la fin de la fonction, s’exécutera dans le pool de threads managés .Net d’arrière-plan.

Si l’appelant est le thread principal, le basculement ne changera pas le thread de l’appelant, ce qui est très pratique. Notez que le basculement nécessite au moins le temps d’une frame.

Basculer vers le thread principal se fait avec await Awaitable.MainThreadAsync();, généralement inutile à appeler.

Malheureusement, sous WebGL (wasm), vous ne pouvez pas utiliser le multithreading .Net, y compris toute méthode Task, ainsi que Awaitable.BackgroundThreadAsync().

Les fonctions Async peuvent être définies comme void, pratique pour divers rappels d’event

Si vous voulez qu’un rappel exécute votre fonction Async, inutile d’encapsuler dans une fonction normale pour appeler votre fonction asynchrone, utilisez async void, comme ci-dessous :

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

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

Notez que async void s’exécute au plus haut niveau, donc les exceptions dans la fonction ne seront pas détectées de l’extérieur, vous devez les gérer vous-même avec un catch.

Forme asynchrone des événements MonoBehaviour

Les événements MonoBehaviour comme Start, Update, etc., peuvent utiliser le mode async, par exemple : async Awaitable Start() {}. Mais rappelez-vous que si Start ou Awake asynchrone ne sont pas terminés, Update sera quand même appelé.

Dernière modification
hugo-builder
hugo-builder · · 自动翻译 2024-12-05... · 55a10c6
Autres contributeurs
...