Unity coroutine vs threads


13

Quelle est la différence entre une coroutine et un fil? Y a-t-il des avantages à utiliser l'un par rapport à l'autre?


3
Coroutines travaillant sur un fil principal.
Woltus


2
Vous ne pouvez pas utiliser de fonctions Unity dans un thread. Les coroutines le peuvent.
Hellium


1
@Hellium Correction, vous ne pouvez pas utiliser de fonctions Unity dans un thread différent, c'est-à-dire que les fonctions Unity peuvent être utilisées dans le thread principal .
Pharap

Réponses:


24

Bien que les Coroutines semblent fonctionner comme des threads à première vue, elles n'utilisent en fait aucun multithreading. Ils sont exécutés séquentiellement jusqu'à ce qu'ilsyield . Le moteur vérifiera toutes les coroutines produites dans le cadre de sa propre boucle principale (à quel moment dépend exactement du type de yield, consultez ce diagramme pour plus d'informations ), continuez-les l'une après l'autre jusqu'à leur prochaine yield, puis continuez avec la boucle principale.

Cette technique a l'avantage que vous pouvez utiliser des coroutines sans les maux de tête causés par des problèmes de multithreading réel. Vous n'obtiendrez aucun blocage, conditions de concurrence ou problèmes de performances causés par les changements de contexte, vous pourrez déboguer correctement et vous n'avez pas besoin d'utiliser des conteneurs de données thread-safe. En effet, lorsqu'une coroutine est en cours d'exécution, le moteur Unity est dans un état contrôlé. Il est sûr d'utiliser la plupart des fonctionnalités Unity.

Avec les threads, en revanche, vous n'avez absolument aucune connaissance de l'état dans lequel se trouve la boucle principale d'Unity pour le moment (elle pourrait en fait ne plus fonctionner du tout). Votre fil pourrait donc causer beaucoup de ravages en faisant quelque chose à la fois, il n'est pas censé faire cette chose. Ne touchez aucune fonctionnalité Unity native à partir d'un sous-thread . Si vous avez besoin de communiquer entre un sous-thread et votre thread principal, demandez au thread d'écrire dans un objet conteneur (!) Adapté aux threads et demandez à un MonoBehaviour de lire ces informations lors des fonctions d'événement Unity habituelles.

L'inconvénient de ne pas faire de "vrai" multithreading est que vous ne pouvez pas utiliser de coroutines pour paralléliser des calculs intensifs sur plusieurs cœurs de processeur. Vous pouvez toutefois les utiliser pour fractionner un calcul sur plusieurs mises à jour. Ainsi, au lieu de geler votre jeu pendant une seconde, vous obtenez simplement un taux de rafraîchissement moyen inférieur sur plusieurs secondes. Mais dans ce cas, vous êtes responsable deyield votre coroutine chaque fois que vous souhaitez autoriser Unity à exécuter une mise à jour.

Conclusion:

  • Si vous souhaitez utiliser l'exécution asynchrone pour exprimer la logique du jeu, utilisez les coroutines.
  • Si vous souhaitez utiliser l'exécution asynchrone pour utiliser plusieurs cœurs de processeur, utilisez des threads.

Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
MichaelHouse

10

Les coroutines sont ce que nous appelons en informatique "multitâche coopératif". Ils permettent à plusieurs flux d'exécution différents de s'entrelacer en coopération. Dans le multitâche coopératif, un seul flux d'exécution a la propriété incontestée du processeur jusqu'à ce qu'il atteigne a yield. À ce stade, Unity (ou tout autre framework que vous utilisez) a la possibilité de basculer vers un flux d'exécution différent. Il obtient ensuite également la propriété incontestée du processeur jusqu'à ce qu'il soit yield.

Les threads sont ce que nous appelons le «multitâche préemptif». Lorsque vous utilisez des threads, le framework se réserve le droit de tout moment d'arrêter votre thread en cours de réflexion et de passer à un autre thread. Peu importe où vous êtes. Vous pouvez même être arrêté à mi-chemin en écrivant une variable dans la mémoire dans certains cas!

Il y a des avantages et des inconvénients pour chacun. Les inconvénients des coroutines sont probablement les plus faciles à comprendre. Tout d'abord, les coroutines s'exécutent toutes sur un seul cœur. Si vous avez un processeur quad core, les coroutines n'utiliseront qu'un des quatre cœurs. Cela simplifie les choses, mais peut être un problème de performances dans certains cas. Le deuxième inconvénient est que vous devez être conscient que toute coroutine peut arrêter tout votre programme simplement en refusant deyield . C'était un problème sur Mac OS9, il y a de nombreuses années. OS9 ne supportait que le multitâche coopératif, sur l'ensemble de l'ordinateur. Si l'un de vos programmes se bloquait, il pourrait arrêter l'ordinateur si violemment que le système d'exploitation ne pourrait même pas rendre le texte du message d'erreur pour vous faire savoir ce qui s'est passé!

Les avantages des coroutines sont qu'elles sont relativement faciles à comprendre. Les erreurs que vous avez sont beaucoup plus prévisibles. Ils nécessitent également généralement moins de ressources, ce qui peut être utile lorsque vous montez dans les dizaines de milliers de coroutines ou de threads. Candid Moon a mentionné dans les commentaires que, si vous n'avez pas étudié correctement les fils, respectez simplement les coroutines, et ils ont raison. Les coroutines sont beaucoup plus simples à utiliser.

Les threads sont une bête complètement différente. Vous devez toujours être sur vos gardes contre la possibilité qu'un autre thread puisse vous interrompre à tout momentet jouer avec vos données. Les bibliothèques de threads fournissent des suites complètes d'outils puissants pour vous aider, tels que des mutex et des variables de condition qui vous aident à dire au système d'exploitation quand il est sûr d'exécuter l'un de vos autres threads et quand il n'est pas sûr. Il existe des cours complets dédiés à la bonne utilisation de ces outils. L'un des problèmes célèbres qui se pose est un «blocage», c'est-à-dire lorsque deux threads sont «bloqués» en attendant que l'autre libère certaines ressources. Un autre problème, très important pour Unity, est que de nombreuses bibliothèques (comme Unity) ne sont pas conçues pour prendre en charge les appels provenant de plusieurs threads. Vous pouvez très facilement casser votre framework si vous ne faites pas attention aux appels autorisés et ceux qui sont interdits.

La raison de cette complexité supplémentaire est en fait très simple. Le modèle multitâche préemptif est vraiment similaire au modèle multithreading qui vous permet non seulement d'interrompre d'autres threads, mais également de les exécuter côte à côte sur différents cœurs. C'est incroyablement puissant, étant le seul moyen de vraiment tirer parti de ces nouveaux processeurs quad core et hexadécimal qui sortent, mais ouvre la boîte de pandores. Les règles de synchronisation sur la façon de gérer ces données dans un monde multithreading sont positivement brutales. Dans le monde C ++, il y a des articles entiers dédiés àMEMORY_ORDER_CONSUME ce qui est un coin itty-bitty-teeny-weenie de la synchronisation multithreading.

Alors les inconvénients du filetage? Simple: ils sont durs. Vous pouvez rencontrer des classes entières de bugs que vous n'avez jamais vus auparavant. Beaucoup sont des soi-disant «heisenbugs» qui apparaissent parfois, puis disparaissent lorsque vous les déboguez. Les outils qui vous sont fournis pour y faire face sont très puissants, mais ils sont également de très bas niveau. Ils sont conçus pour être efficaces sur les architectures des puces modernes plutôt que d'être conçus pour être faciles à utiliser.

Cependant, si vous souhaitez utiliser toute la puissance de votre processeur, c'est l'outil dont vous avez besoin. En outre, il existe en fait des algorithmes plus faciles à comprendre en multithreading qu'avec les coroutines simplement parce que vous laissez le système d'exploitation gérer toutes les questions sur les endroits où les interruptions peuvent avoir lieu.

Le commentaire de Candid Moon de s'en tenir aux coroutines est également ma recommandation. Si vous voulez la puissance des threads, engagez-vous. Sortez et apprenez vraiment les discussions, officiellement. Nous avons eu plusieurs décennies pour découvrir comment organiser la meilleure façon de penser aux threads afin d'obtenir rapidement des résultats fiables et reproductibles et d'ajouter des performances au fur et à mesure. Par exemple, tous les cours sensés enseigneront les mutex avant d'enseigner les variables de condition. Tous les sains d' esprit des cours qui Atomics de couverture enseigneront entièrement mutex et les variables de condition avant même mentionner l'existence Atomics. (Remarque: il n'y a pas de tutoriel sain sur l'atomique.) Essayez d'apprendre le filetage au coup par coup, et vous implorez une migraine.


Les threads ajoutent de la complexité principalement dans les cas où les opérations et les dépendances sont entrelacées. Si la boucle de jeu principale contient trois fonctions, x (), y () et z (), et qu'aucune d'entre elles n'affecte tout ce dont une autre a besoin, démarrer toutes les trois simultanément mais attendre que tout se termine avant de continuer peut permettre à quelqu'un de bénéficier d'une machine multicœur sans avoir à ajouter trop de complexité. Alors que les variables de condition sont généralement utilisées en conjonction avec des mutex, le paradigme des étapes parallèles indépendantes n'a pas vraiment besoin de mutex en tant que tels ...
supercat

... mais simplement un moyen pour les threads qui gèrent y () et z () d'attendre jusqu'à ce qu'ils se déclenchent, puis un moyen pour le thread principal d'attendre, après avoir exécuté x (), jusqu'à y () et z () a complété.
supercat

@supercat Vous devez toujours avoir une synchronisation pour faire la transition entre des phases parallèles indépendantes et des phases séquentielles. Parfois, ce n'est rien de plus qu'un join(), mais vous avez besoin de quelque chose. Si vous n'avez pas d'architecte qui a conçu un système pour effectuer la synchronisation pour vous, vous devez l'écrire vous-même. En tant que personne qui fait des trucs multithread, je trouve que les modèles mentaux des gens sur le fonctionnement des ordinateurs doivent être ajustés avant de faire la synchronisation en toute sécurité (c'est ce que les bons cours enseigneront)
Cort Ammon

Bien sûr, il doit y avoir une synchronisation, et atteindre une efficacité maximale nécessitera généralement quelque chose de plus join. Mon point de vue était que la poursuite d'une fraction modérée des avantages de performance possibles du threading, facilement, peut parfois être meilleure que l'utilisation d'une approche de threading plus compliquée pour récolter une fraction plus importante.
supercat

À propos des nouveaux types de bugs: notez également que si vous ne pouvez pas prouver logiquement que le code est sûr, il est impossible de savoir à quel point il sera bogué. Bien que l'ordre d'exécution puisse être indéfini, il emprunte souvent des chemins similaires chaque fois que vous l'exécutez sur 1 machine. Vous constaterez souvent qu'il échoue durement et souvent sur une fraction de machines, et vous ne pouvez pas tester sur toutes les machines possibles. Au travail, nous avions un bug qui ne se manifestait que sur 1 de nos machines, uniquement si la journalisation était désactivée; plusieurs personnes ont mis beaucoup de temps à le retrouver. Je ne veux pas ça à l'état sauvage. Ne vous contentez pas de tester la synchronisation, prouvez-le logiquement.
Aaron

2

Dans les termes les plus simples possibles ...


Fils

Un thread ne décide pas quand il cède, le système d'exploitation (le 'OS', par exemple Windows) décide quand un thread est cédé. Le système d'exploitation est presque entièrement responsable de la planification des threads, il décide quels threads exécuter, quand les exécuter et pendant combien de temps.

De plus, un thread peut être exécuté de manière synchrone (un thread après l'autre) ou asynchrone (différents threads s'exécutant sur différents cœurs de processeur). La possibilité de s'exécuter de manière asynchrone signifie que les threads peuvent faire plus de travail dans le même temps (car les threads font littéralement deux choses en même temps). Même les threads synchrones font beaucoup de travail si le système d'exploitation est bon pour les planifier.

Cependant, cette puissance de traitement supplémentaire s'accompagne d'effets secondaires. Par exemple, si deux threads tentent d'accéder à la même ressource (par exemple une liste) et que chaque thread peut être arrêté de manière aléatoire à n'importe quel point du code, les modifications du deuxième thread peuvent interférer avec les modifications apportées par le premier thread. (Voir aussi: Conditions de course et impasse .)

Les threads sont également considérés comme «lourds» car ils ont beaucoup de frais généraux, ce qui signifie une pénalité de temps considérable lors du changement de threads.


Coroutines

Contrairement aux threads, les coroutines sont complètement synchrones, une seule coroutine peut être exécutée à un moment donné. De plus, les coroutines peuvent choisir quand céder, et peuvent donc choisir de céder à un point du code qui est convinient (par exemple à la fin d'un cycle de boucle). Cela a l'avantage de problèmes comme les conditions de concurrence et les blocages beaucoup plus faciles à éviter, ainsi que la coopération entre les coroutines.

Cependant, c'est aussi une responsabilité majeure, si une coroutine ne donne pas correctement, elle pourrait finir par consommer beaucoup de temps processeur et elle peut toujours causer des bugs si elle modifie les ressources partagées de manière incorrecte.

Les coroutines ne nécessitent généralement pas de changement de contexte et sont donc rapides à activer et désactiver et sont assez légères.


En résumé:

Fil:

  • Synchrone ou asynchrone
  • Cédé par OS
  • Cédé au hasard
  • Lourd

Coroutine:

  • Synchrone
  • Se donne soi-même
  • Rendements au choix
  • Poids léger

Les rôles des threads et des coroutines sont très similaires, mais ils diffèrent dans la façon dont ils font le travail, ce qui signifie que chacun est mieux adapté à différentes tâches. Les threads sont les meilleurs pour les tâches où ils peuvent se concentrer sur faire quelque chose par eux-mêmes sans être interrompus, puis signaler quand ils ont terminé. Les coroutines sont idéales pour les tâches qui peuvent être effectuées en de nombreuses petites étapes et les tâches qui nécessitent un traitement coopératif des données.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.