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:
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.
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.
la source
Réponses:
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.
la source
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.
la source
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 un
ReaderWriterLockSlim
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
delegate
objet à 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.la source
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:
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:
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.la source