web-dev-qa-db-fra.com

Dans Unity / C #, l'async /Net de .Net commence-t-il, littéralement, un autre thread?

Important pour tous ceux qui recherchent ce sujet difficile dans Unity en particulier,

assurez-vous de voir une autre question que j'ai posée qui a soulevé des problèmes clés connexes:

Dans Unity en particulier, "où" un attente revient-il littéralement?


Pour les experts C #, Unity est monothread1

Il est courant de faire des calculs et autres sur un autre thread.

Lorsque vous faites quelque chose sur un autre thread, vous utilisez souvent async/wait car, euh, tous les bons programmeurs C # disent que c'est le moyen facile de le faire!

void TankExplodes() {

    ShowExplosion(); .. ordinary Unity thread
    SoundEffects(); .. ordinary Unity thread
    SendExplosionInfo(); .. it goes to another thread. let's use 'async/wait'
}

using System.Net.WebSockets;
async void SendExplosionInfo() {

    cws = new ClientWebSocket();
    try {
        await cws.ConnectAsync(u, CancellationToken.None);
        ...
        Scene.NewsFromServer("done!"); // class function to go back to main tread
    }
    catch (Exception e) { ... }
}

OK, donc quand vous faites cela, vous faites tout "comme vous le faites normalement" lorsque vous lancez un thread d'une manière plus conventionnelle dans Unity/C # (donc en utilisant Thread ou quoi que ce soit ou en laissant un plugin natif le faire ou le système d'exploitation ou quoi que ce soit).

Tout fonctionne très bien.

En tant que programmeur Unity boiteux qui ne connaît que suffisamment C # pour arriver à la fin de la journée, j'ai toujours supposé que le modèle asynchrone/attente ci-dessus lance littéralement un autre thread .

En fait, le code ci-dessus lance-t-il littéralement un autre thread , ou c # /. Net Utilisez une autre approche pour réaliser des tâches lorsque vous utilisez le natty async/modèle d'attente?

Peut-être que cela fonctionne différemment ou spécifiquement dans le moteur Unity de "l'utilisation de C # en général"? (IDK?)

Notez que dans Unity, qu'il s'agisse ou non d'un thread affecte considérablement la façon dont vous devez gérer les étapes suivantes. D'où la question.


Problème: Je me rends compte qu'il y a beaucoup de discussions sur "est en attente d'un fil", mais, (1) je n'ai jamais vu cette discussion/réponse dans le paramètre Unity (cela fait-il une différence? IDK?) (2) Je n'ai tout simplement jamais vu de réponse claire!


1 Certains calculs auxiliaires (par exemple, la physique, etc.) sont effectués sur d'autres threads, mais le "moteur de jeu basé sur les cadres" est un thread pur. (Il est impossible "d'accéder" au thread principal du châssis du moteur de quelque manière que ce soit: lors de la programmation, par exemple, d'un plugin natif ou d'un calcul sur un autre thread, vous laissez simplement des marqueurs et des valeurs pour les composants sur le thread du châssis du moteur à regarder et utiliser quand ils exécutent chaque image.)

14
Fattie

Je n'aime pas répondre à ma propre question, mais il s'avère qu'aucune des réponses ici n'est totalement correcte. (Cependant, beaucoup/toutes les réponses ici sont extrêmement utiles de différentes manières).

En fait, la réponse réelle peut être formulée en quelques mots:

Sur quel thread l'exécution reprend après une attente est contrôlée par SynchronizationContext.Current.

C'est tout.

Ainsi, dans toute version particulière d'Unity (et notez que, au moment de la rédaction de 2019, ils changent radicalement Unity - https: // unité .com/dots ) - ou bien n'importe quel environnement C # /. Net du tout - la question sur cette page peut être répondue correctement.

Les informations complètes sont apparues lors de cette AQ de suivi:

https://stackoverflow.com/a/55614146/294884

2
Fattie

Cette lecture: les tâches ne sont (toujours) pas des threads et async n'est pas parallèle pourrait vous aider à comprendre ce qui se passe sous le capot. En bref, pour que votre tâche s'exécute sur un thread séparé, vous devez appeler

Task.Run(()=>{// the work to be done on a separate thread. }); 

Ensuite, vous pouvez attendre cette tâche là où vous en avez besoin.

Pour répondre à ta question

"En fait, le code ci-dessus lance-t-il littéralement un autre thread, ou c # /. Net utilise-t-il une autre approche pour réaliser des tâches lorsque vous utilisez le modèle async/wait natty?"

Non - ce n'est pas le cas.

Si tu l'as fait

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

Ensuite, cws.ConnectAsync(u, CancellationToken.None) s'exécuterait sur un thread séparé.

En réponse au commentaire voici le code modifié avec plus d'explications:

    async void SendExplosionInfo() {

        cws = new ClientWebSocket();
        try {
            var myConnectTask = Task.Run(()=>cws.ConnectAsync(u, CancellationToken.None));

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // class function to go back to main tread
        }
        catch (Exception e) { ... }
    }

Vous n'en aurez peut-être pas besoin sur un thread séparé, car le travail asynchrone que vous faites n'est pas lié au processeur (ou du moins il semble). Ainsi, vous devriez être bien avec

 try {
            var myConnectTask =cws.ConnectAsync(u, CancellationToken.None);

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // continue from here
        }
        catch (Exception e) { ... }
    }

Séquentiellement, il fera exactement la même chose que le code ci-dessus mais sur le même thread. Il permettra au code après " ConnectAsync " de s'exécuter et ne s'arrêtera que pour attendre la fin de " ConnectAsync "où il est dit attendre et depuis" ConnectAsync "n'est pas lié au CPU (ce qui le rend un peu parallèle dans le sens où le travail est fait ailleurs, c'est-à-dire la mise en réseau) aura suffisamment de jus pour exécuter vos tâches, à moins que votre code entre "...." nécessite également beaucoup de travail lié au CPU, que vous préférez exécuter en parallèle.

Vous pouvez également éviter d'utiliser async void car il n'est là que pour les fonctions de haut niveau. Essayez d'utiliser la tâche asynchrone dans la signature de votre méthode. Vous pouvez en savoir plus à ce sujet ici.

17
agfc

Non, async/wait ne signifie pas - un autre thread. Il peut démarrer un autre thread mais ce n'est pas obligatoire.

Ici vous pouvez trouver un article assez intéressant à ce sujet: https://blogs.msdn.Microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is- non parallèle /

5
Adam Jachocki

Avis important

Tout d'abord, il y a un problème avec la première déclaration de votre question.

L'unité est monothread

L'unité n'est pas monothread; en fait, Unity est un environnement multi-thread . Pourquoi? Accédez simplement à la page Web officielle de Unity et lisez-la:

Système multithread haute performance: Utilisez pleinement les processeurs multicœurs disponibles aujourd'hui (et demain), sans programmation lourde. Notre nouvelle base pour permettre des performances élevées est composée de trois sous-systèmes: le système de travail C #, qui vous offre un bac à sable sûr et facile pour écrire du code parallèle; l'Entity Component System (ECS), un modèle d'écriture de code haute performance par défaut, et le Burst Compiler, qui produit du code natif hautement optimisé.

Le moteur Unity 3D utilise un Runtime .NET appelé "Mono" qui est multithread par sa nature. Pour certaines plateformes, le code managé sera transformé en code natif, il n'y aura donc pas de .NET Runtime. Mais le code lui-même sera de toute façon multithread.

Veuillez donc ne pas déclarer de faits trompeurs et techniquement incorrects.

Ce que vous discutez, c'est simplement une déclaration qui il y a un thread principal dans Unity qui traite la charge de travail principale d'une manière basée sur les trames. C'est vrai. Mais ce n'est pas quelque chose de nouveau et d'unique! Par exemple. une application WPF exécutée sur .NET Framework (ou .NET Core à partir de 3.0) a également un thread principal (souvent appelé thread d'interface utilisateur), et la charge de travail est traitée sur ce thread dans un cadre en utilisant le WPF Dispatcher (file d'attente du répartiteur, opérations, trames, etc.) Mais tout cela ne rend pas l'environnement monothread! C'est juste un moyen de gérer la logique de l'application.


Une réponse à votre question

Remarque: ma réponse ne s'applique qu'aux instances Unity qui exécutent un environnement .NET Runtime (Mono). Pour les instances qui convertissent le code C # géré en code C++ natif et créent/exécutent des binaires natifs, ma réponse est probablement au moins inexacte.

Vous écrivez:

Lorsque vous faites quelque chose sur un autre thread, vous utilisez souvent async/wait car, euh, tous les bons programmeurs C # disent que c'est le moyen facile de le faire!

Les mots clés async et await en C # ne sont qu'un moyen d'utiliser le TAP ( Task-Asynchronous Pattern ).

Le TAP est utilisé pour les opérations asynchrones arbitraire. D'une manière générale, il n'y a pas de thread . Je recommande fortement de lire cet article de Stephen Cleary intitulé "Il n'y a pas de fil" . (Stephen Cleary est un gourou de la programmation asynchrone réputé si vous ne le savez pas.)

La principale cause de l'utilisation de la fonction async/await Est une opération asynchrone. Vous utilisez async/await Non pas parce que "vous faites quelque chose sur un autre thread", mais parce que vous avez une opération asynchrone que vous devez attendre. Qu'il y ait un thread d'arrière-plan ou non, cette opération s'exécutera - cela n'a pas d'importance pour vous (enfin, presque; voir ci-dessous). Le TAP est un niveau d'abstraction qui masque ces détails.

En fait, le code ci-dessus lance-t-il littéralement un autre thread , ou c # /. Net Utilisez une autre approche pour réaliser des tâches lorsque vous utilisez le natty async/modèle d'attente?

La bonne réponse est: cela dépend .

  • si ClientWebSocket.ConnectAsync lève immédiatement une exception de validation d'argument (par exemple un ArgumentNullException lorsque uri est nul), pas de nouveau thread sera démarré
  • si le code de cette méthode se termine très rapidement, le résultat de la méthode sera disponible de manière synchrone, aucun nouveau thread ne sera démarré
  • si l'implémentation de la méthode ClientWebSocket.ConnectAsync est une opération asynchrone pure sans aucun thread impliqué, votre appel méthode sera "suspendu" (en raison de await) - donc aucun nouveau thread ne sera démarré
  • si l'implémentation de la méthode implique des threads et que le TaskScheduler actuel est capable de planifier cet élément de travail sur un thread de pool de threads en cours d'exécution, pas de nouveau thread sera démarré; à la place, l'élément de travail sera mis en file d'attente sur un thread déjà en cours d'exécution pool de threads
  • si tous les threads du pool de threads sont déjà occupés, le runtime peut générer de nouveaux threads en fonction de sa configuration et de l'état actuel du système, donc oui - un nouveau thread peut être démarré et l'élément de travail sera mis en file d'attente sur ce nouveau thread

Vous voyez, c'est assez complexe. Mais c'est exactement la raison pour laquelle le modèle TAP et la paire de mots-clés async/await Ont été introduits dans C #. Ce sont généralement les choses qu'un développeur ne veut pas déranger, alors cachons ces choses dans le runtime/framework.

@agfc déclare une chose pas tout à fait correcte:

"Cela n'exécutera pas la méthode sur un thread d'arrière-plan"

await cws.ConnectAsync(u, CancellationToken.None);

"Mais ça va"

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

Si l'implémentation de la partie synchrone de ConnectAsync est minuscule, le planificateur de tâches peut-être exécute cette partie de manière synchrone dans les deux cas. Ces deux extraits peuvent donc être exactement les mêmes en fonction de l'implémentation de la méthode appelée.

Notez que le ConnectAsync a un suffixe Async et renvoie un Task. Il s'agit d'une information basée sur la convention que la méthode est vraiment asynchrone. Dans de tels cas, vous devriez toujours préférer await MethodAsync() plutôt que await Task.Run(() => MethodAsync()).

Autres lectures intéressantes:

4
dymanoid

Le code après une attente continuera sur un autre thread de pool de threads. Cela peut avoir des conséquences lorsque vous traitez des références non thread-safe dans une méthode, comme Unity, DbContext d'EF et de nombreuses autres classes, y compris votre propre code personnalisé.

Prenons l'exemple suivant:

    [Test]
    public async Task TestAsync()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread Before Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = await names;
            Console.WriteLine("Thread After Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Le résultat:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 29
Thread Before Await: 29
Thread After Await: 12

1 passed, 0 failed, 0 skipped, took 3.45 seconds (NUnit 3.10.1).

Notez que le code avant et après le ToListAsync s'exécute sur le même thread. Donc, avant d'attendre l'un des résultats, nous pouvons continuer le traitement, bien que les résultats de l'opération asynchrone ne soient pas disponibles, juste le Task qui est créé. (qui peut être abandonné, attendu, etc.) Une fois que nous avons mis un await dans, le code suivant sera effectivement séparé comme une continuation, et/pourra revenir sur un autre thread.

Cela s'applique lorsque vous attendez une opération asynchrone en ligne:

    [Test]
    public async Task TestAsync2()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = await context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Production:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async/Await: 6
Thread After Async/Await: 33

1 passed, 0 failed, 0 skipped, took 4.38 seconds (NUnit 3.10.1).

Encore une fois, le code après l'attente est exécuté sur un autre thread de l'original.

Si vous souhaitez vous assurer que le code appelant le code asynchrone reste sur le même thread, vous devez utiliser le Result sur le Task pour bloquer le thread jusqu'à ce que la tâche asynchrone se termine:

    [Test]
    public void TestAsync3()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = names.Result;
            Console.WriteLine("Thread After Result: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Production:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 20
Thread After Async: 20
Thread After Result: 20

1 passed, 0 failed, 0 skipped, took 4.16 seconds (NUnit 3.10.1).

En ce qui concerne Unity, EF, etc., vous devez être prudent lorsque vous utilisez généreusement async lorsque ces classes ne sont pas thread-safe. Par exemple, le code suivant peut entraîner un comportement inattendu:

        using (var context = new TestDbContext())
        {
            var ids = await context.Customers.Select(x => x.CustomerId).ToListAsync();
            foreach (var id in ids)
            {
                var orders = await context.Orders.Where(x => x.CustomerId == id).ToListAsync();
                // do stuff with orders.
            }
        }

En ce qui concerne le code, cela semble correct, mais DbContext est pas thread-safe et la référence DbContext unique s'exécutera sur un thread différent lorsqu'il est interrogé pour les commandes en fonction de l'attente du client initial charge.

Utilisez async lorsqu'il y a un avantage significatif sur les appels synchrones, et vous êtes sûr que la suite n'accédera qu'au code thread-safe.

1
Steve Py