Comment rendre le message passant entre les threads dans un moteur multithread moins encombrant?

18

Le moteur C ++ sur lequel je travaille actuellement est divisé en plusieurs gros threads: génération (pour créer mon contenu procédural), gameplay (pour l'IA, les scripts, la simulation), la physique et le rendu.

Les threads communiquent entre eux par le biais de petits objets de message, qui passent d'un thread à l'autre. Avant de marcher, un thread traite tous ses messages entrants - mises à jour pour transformer, ajouter et supprimer des objets, etc. Parfois, un thread (Génération) crée quelque chose (Art) et le transmet à un autre thread (Rendu) pour une propriété permanente.

Au début du processus, j'ai remarqué quelques choses:

  1. Le système de messagerie est encombrant. Créer un nouveau type de message signifie sous-classer la classe Message de base, créer une nouvelle énumération pour son type et écrire une logique pour la façon dont les threads doivent interpréter le nouveau type de message. C'est un ralentisseur pour le développement et est sujet aux erreurs de style typo. (Sidenote - y travailler m'a fait apprécier à quel point les langages dynamiques peuvent être géniaux!)

    Y a-t-il une meilleure manière de faire cela? Dois-je utiliser quelque chose comme boost :: bind pour rendre cela automatique? Je crains que si je le fais, je perds la capacité de dire, de trier les messages en fonction du type ou quelque chose. Je ne sais pas si ce type de gestion deviendra même nécessaire.

  2. Le premier point est important car ces fils communiquent beaucoup. Créer et transmettre des messages est un élément important pour faire bouger les choses. J'aimerais rationaliser ce système, mais aussi être ouvert à d'autres paradigmes qui pourraient être tout aussi utiles. Existe-t-il différents modèles multithread auxquels je devrais penser pour faciliter la tâche?

    Par exemple, certaines ressources sont rarement écrites, mais fréquemment lues à partir de plusieurs threads. Dois-je être ouvert à l'idée d'avoir des données partagées, protégées par des mutex, auxquelles tous les threads peuvent accéder?

C'est la première fois que je conçois quelque chose avec le multithreading à l'esprit. À ce stade précoce, je pense que ça se passe très bien (compte tenu), mais je m'inquiète de la mise à l'échelle et de ma propre efficacité à mettre en œuvre de nouvelles choses.

Viande de rapace
la source
6
Il n'y a vraiment pas une seule question dirigée ici, et en tant que tel, ce post ne convient pas au style Q&A de ce site. Je vous recommande de diviser votre message en messages distincts, un par question, et de recentrer les questions afin qu'ils posent des questions sur un problème spécifique que vous rencontrez réellement au lieu d'une vague collection de conseils ou de conseils.
2
Si vous souhaitez engager une conversation plus générale, je vous recommande d'essayer ce message sur les forums de gamedev.net . Comme l'a dit Josh, puisque votre "question" n'est pas une seule question spécifique, il serait assez difficile de répondre au format StackExchange.
Cypher
Merci pour les commentaires les amis! J'espérais en quelque sorte que quelqu'un avec plus de connaissances pourrait avoir une seule ressource / expérience / paradigme qui pourrait résoudre plusieurs de mes problèmes à la fois. J'ai l'impression qu'une grande idée pourrait synthétiser mes divers problèmes en une chose qui me manque, et je pensais que quelqu'un avec plus d'expérience que moi pourrait le reconnaître ... Mais peut-être pas, et de toute façon - des points pris !
Raptormeat
J'ai renommé votre titre pour qu'il soit plus spécifique à la transmission des messages, car les questions de type «conseils» impliquent qu'il n'y a pas de problème spécifique à résoudre (et donc je conclurais aujourd'hui comme «pas une vraie question»).
Tetrad
Êtes-vous sûr d'avoir besoin de threads séparés pour la physique et le gameplay? Ces deux semblent très imbriqués. De plus, il est difficile de savoir comment offrir des conseils sans savoir comment chacun communique et avec qui.
Nicol Bolas

Réponses:

10

Pour votre problème plus large, envisagez de trouver des moyens de réduire autant que possible la communication entre les threads. Il est préférable d'éviter complètement les problèmes de synchronisation, si vous le pouvez. Cela peut être réalisé en tamponnant deux fois vos données, en introduisant une latence de mise à jour unique mais en facilitant considérablement la tâche de travailler avec des données partagées.

En passant, avez-vous envisagé de ne pas threader par sous-système, mais plutôt d'utiliser la génération de threads ou des pools de threads pour effectuer un fork par tâche? (Voir ceci en ce qui concerne votre problème spécifique, pour le regroupement de threads.) Ce court document décrit brièvement le but et l'utilisation du modèle de pool. Voir ces réponses informativesaussi. Comme indiqué ici, les pools de threads améliorent l'évolutivité en bonus. Et c'est "écrire une fois, utiliser n'importe où", au lieu d'avoir à obtenir des threads basés sur des sous-systèmes pour jouer correctement chaque fois que vous écrivez un nouveau jeu ou un nouveau moteur. Il existe également de nombreuses solutions de pool de threads tiers solides. Il serait plus simple de commencer par la génération de threads et de vous diriger vers des pools de threads plus tard, si la surcharge de génération et de destruction de threads doit être atténuée.

Ingénieur
la source
1
Des recommandations pour les bibliothèques de pool de threads spécifiques à vérifier?
imre
Nick - merci beaucoup pour la réponse. En ce qui concerne votre premier point, je pense que c'est une excellente idée et probablement la direction dans laquelle je vais avancer. Pour le moment, il est suffisamment tôt pour que je ne sache pas encore ce qui devrait être à double tampon. Je garderai cela à l'esprit car cela se solidifie avec le temps. À votre deuxième point - merci pour la suggestion! Oui, les avantages des tâches de thread sont clairs. Je vais lire vos liens et y réfléchir. Je ne suis pas sûr à 100% si cela fonctionnera pour moi / comment le faire fonctionner pour moi, mais je vais certainement y réfléchir sérieusement. Merci!
Raptormeat
1
@imre consultez la bibliothèque Boost - ils ont des futurs, qui sont un moyen agréable / facile d'approcher ces choses.
Jonathan Dickinson
5

Vous avez posé des questions sur différentes conceptions multithread. Un de mes amis m'a parlé de cette méthode que je trouvais plutôt cool.

L'idée est qu'il y aurait 2 copies de chaque entité de jeu (gaspillage, je sais). Une copie serait la copie actuelle et l'autre serait la copie précédente. La copie actuelle est strictement écrite et la copie précédente est strictement en lecture seule . Lorsque vous allez mettre à jour, vous attribuez des plages de votre liste d'entités à autant de threads que vous le souhaitez. Chaque thread a un accès en écriture aux copies présentes dans la plage affectée et chaque thread a un accès en lecture à toutes les copies passées des entités, et peut ainsi mettre à jour les copies présentes attribuées en utilisant les données des copies passées sans verrouillage. Entre chaque image, la copie actuelle devient la copie précédente, mais vous voulez gérer l'échange de rôles.

John McDonald
la source
4

Nous avons eu le même problème, uniquement avec C #. Après avoir longuement réfléchi à la facilité (ou à l'absence de facilité) de créer de nouveaux messages, le mieux que nous puissions faire était de leur créer un générateur de code. C'est un peu moche, mais utilisable: étant donné seulement une description du contenu du message, il génère la classe de message, les énumérations, le code de gestion des espaces réservés, etc.

Je ne suis pas entièrement satisfait de cela, mais c'est mieux que d'écrire tout ce code à la main.

Concernant les données partagées, la meilleure réponse est bien sûr "ça dépend". Mais généralement, si certaines données sont lues souvent et nécessaires à de nombreux threads, le partage en vaut la peine. Pour la sécurité des threads, votre meilleur pari est de le rendre immuable , mais si cela est hors de question, mutex pourrait le faire. En C # il y a unReaderWriterLockSlim classe, spécialement conçue pour de tels cas; Je suis sûr qu'il existe un équivalent C ++.

Une autre idée pour la communication des threads, qui résout probablement votre premier problème, est de passer des gestionnaires au lieu de messages. Je ne sais pas comment résoudre cela en C ++, mais en C #, vous pouvez envoyer un delegateobjet à un autre thread (comme dans, l'ajouter à une sorte de file d'attente de messages) et appeler réellement ce délégué à partir du thread de réception. Cela permet de créer des messages "ad hoc" sur place. Je n'ai fait que jouer avec cette idée, je ne l'ai jamais réellement essayée en production, donc ça pourrait être mauvais en fait.

Ça ne fait rien
la source
Merci pour toutes les informations! La dernière partie sur les gestionnaires est similaire à ce que je mentionnais à propos de l'utilisation de liaisons ou de foncteurs pour passer des fonctions. J'aime un peu l'idée - je pourrais l'essayer et voir si ça craint ou si c'est génial: D Pour commencer, il suffit de créer une classe CallDelegateMessage et de tremper mon orteil dans l'eau.
Raptormeat
1

Je ne suis que dans la phase de conception d'un code de jeu fileté, donc je ne peux que partager mes pensées, pas toute expérience réelle. Cela dit, je pense dans le sens suivant:

  • La plupart des données de jeu doivent être partagées pour un accès en lecture seule .
  • L'écriture de données est possible à l'aide d'une sorte de messagerie.
  • Pour éviter de mettre à jour les données pendant qu'un autre thread les lit, la boucle de jeu comporte deux phases distinctes: lecture et mise à jour.
  • En phase de lecture:
  • Toutes les données partagées sont en lecture seule pour tous les threads.
  • Les threads peuvent calculer des éléments (en utilisant le stockage local des threads) et produire des demandes de mise à jour , qui sont essentiellement des objets de commande / message, placés dans une file d'attente, à appliquer ultérieurement.
  • Dans la phase de mise à jour:
  • Toutes les données partagées sont en écriture seule. Les données doivent être supposées dans un état inconnu / instable.
  • C'est là que les objets de demande de mise à jour sont traités.

Je pense (bien que je ne sois pas sûr) en théorie, cela devrait signifier que pendant les phases de lecture et de mise à jour, un certain nombre de threads peuvent s'exécuter simultanément avec une synchronisation minimale. Dans la phase de lecture, personne n'écrit les données partagées, donc aucun problème de concurrence ne devrait survenir. La phase de mise à jour, eh bien, c'est plus délicat. Les mises à jour parallèles sur le même élément de données seraient un problème, donc une synchronisation est nécessaire ici. Cependant, je pourrais toujours exécuter un nombre arbitraire de threads de mise à jour, tant qu'ils fonctionnent sur différents ensembles de données.

Dans l'ensemble, je pense que cette approche se prêterait bien à un système de pool de threads. Les parties problématiques sont:

  • Synchronisation des threads de mise à jour (assurez-vous qu'aucun thread multiple n'essaie de mettre à jour le même ensemble de données).
  • Assurez-vous qu'en phase de lecture, aucun thread ne peut accidentellement écrire des données partagées. J'ai peur qu'il y ait trop de place pour les erreurs de programmation, et je ne sais pas combien d'entre elles pourraient être facilement détectées par les outils de débogage.
  • Écrire du code de manière à ce que vous ne puissiez pas compter sur vos résultats intermédiaires pour les lire immédiatement. Autrement dit, vous ne pouvez pas écrire x += 2; if (x > 5) ...si x est partagé. Vous devez soit faire une copie locale de x, soit produire une demande de mise à jour, et ne faire que le conditionnel lors de la prochaine exécution. Ce dernier signifierait beaucoup de code supplémentaire de conservation de l'état local du thread.
imre
la source