"thread-safe" std::vector - C++ - Programmation
Marsh Posté le 11-03-2012 à 23:33:58
ça permet de dessiner quoi ces tableaux ?
Marsh Posté le 11-03-2012 à 23:41:00
Idées en vrac :
- Quand tu a un rendu à faire, tu peux utiliser un "snapshot" du dernier état stable de ta logique (enfin de tes tableaux de positions). Pour commencer, tu peux prendre un mutex le temps de copier les tableaux et tu travailles sur la copie.
- Utiliser les lock de type multiple readers / single-writer, c'est pratique si tu as plusieurs threads qui ne font que lire (audio, vidéo)
Marsh Posté le 12-03-2012 à 19:28:03
Bonsoir
Terminapor a écrit : ça permet de dessiner quoi ces tableaux ? |
Des lignes brisées dont chaque sommet voit ses coordonnées (3 "double" : x, y et z) enregistrées dans les tableaux en question.
ITM a écrit : Idées en vrac : |
J'ai bien peur que la copie de tableau ne soit la meilleure solution dans mon cas : leur taille est assez importante. Pour donner une idée, chaque tableau peut contenir jusqu'à 90 000 points soit 270 000 "double". (j'aurais effectivement du préciser leur taille). Cela peut-il effectivement poser problème ?
En ce qui concerne le lock multiple readers/single-writer, me recommanderiez vous l'utilisation de boost::shared_mutex pour cela ?
Ou bien je ne suis pas obligé de passer par là ?
Merci pour vos réponses,
A+.
Marsh Posté le 12-03-2012 à 21:00:59
La copie de tableau était un exemple pour déjà voir une amélioration significative.
Si cette copie est vraiment trop coûteuse, il faut peut-être faire autrement : le thread de calcul travaille sur plusieurs zones de mémoires successivement et le thread de rendu vient verrouiller la zone la plus fraîche le temps de faire son boulot.
Il y a sans doute mieux à faire, je serais curieux de voir les idées d'autres personnes
Marsh Posté le 13-03-2012 à 22:45:21
ITM a écrit : |
Bonsoir,
merci encore pour tes réponses.
Pourrais tu cependant développer la partie en gras stp ? (concrètement, comment faire ?)
Merci.
PS : je dois retirer très régulièrement le premier élément de mes vector (càd delete(vec.front()) + vec.erase(vec.begin()) ). Cette opération est-elle couteuse avec des tableaux content beaucoup de données ?
A+,
Marsh Posté le 13-03-2012 à 22:47:25
Ben une suppression techniquement fait une réallocation mémoire, donc en gros il alloue une nouvelle zone avec la nouvelle taille, et rempli cette nouvelle puis supprime l'ancienne.. (à moins que je ne me trompe)
Ptet que tu devrais essayer avec des listes chaînées ?
Marsh Posté le 15-03-2012 à 18:00:59
Bonjour,
Puisque je n'ai pas pu trouvé de solution simple à mon problème dans l'immédiat, je l'ai contourné.
Au lieu de libérer la mémoire au fur et à mesure, le thread d'affichage ignore simplement les données du tableau "en trop.".
Pour le problème des écritures/lectures concurrentes (autre que DELETE), j'ai remplacé mon std::vector par un concurrent_vector
http://msdn.microsoft.com/fr-fr/library/ee355345.aspx
J'étudierai davantage les autres solutions plus tard (listes chaînées, ...), parce qu'il n'est toujours pas résolu. Mais je dois avancer.
Merci pour votre aide.
A+,
Marsh Posté le 15-04-2012 à 11:54:42
Bonjour,
Je up de nouveau le sujet parce que j'ai besoin de trouver une vraie solution à mon problème..
En fait, même sans supprimer des objets au fur et à mesure dans mon thread d'écriture, le programme arrive à planter en utilisant des std::vector.
Quand j'utilise la Concurreny runtime en revanche pas de soucis. Mais (arrêtez moi si je me trompe), ça ne fonctionne que sous windows.
Autre question : comment mon programme peut planter alors que le thread de lecture se contente de lire un tableau rempli auparavant par le thread d'écriture, sachant que la mémoire a été correctement allouée puisque l'objet n'est indexé dans le vector qu'après l'allocation new ?
Je ne comprends vraiment pas comment sans DELETE, ça continue de planter...
PS : ça plante beaucoup plus vite en mode débogage sous visual c++ 2010 qu'en exécution normale, si ça peut aider.
A+,
Marsh Posté le 15-04-2012 à 14:50:43
Ben, montre ton code
Marsh Posté le 15-04-2012 à 15:00:21
Pas bête
Bon le code entier est long alors voici des portions extrêmement simplifiées :
1) Ecriture (appelée un très grand nombre de fois à chaque itération de la boucle principale du thread de calculs)
Code :
|
2) Lecture
(appelée à chaque boucle du thread d'affichage)
Code :
|
Le code complet est téléchargeable ici, mais pas tout à fait à jour (j'ai simplifié certains trucs de mon côté) :
http://orbit.sciencestechniques.fr/app/0.1b/source.zip
Merci encore pour l'aide...
Marsh Posté le 15-04-2012 à 16:13:19
juste une proposition pour ton problème.
De manière générale, tu gardes un seul tableau de données. Ton thread de rendu lit sans se soucier de rien. Ton thread de gameplay stocke ses modifications dans une structure associée à tes données (des sortes de commandes, mais ces commandes peuvent probablement être mergées en une seule, genre une seule matrice qui représente l'intégralité de tes transformations sur la frame) Et tu as un point de synchro entre tes deux threads où tu appliques ce que le gameplay a calculé à tes structures avant de lancer ton rendu sur la scène.
Marsh Posté le 15-04-2012 à 19:27:25
Bonsoir,
Effectivement c'est une piste mais la difficulté c'est justement de déterminer cette synchro.
Quand doit-on synchroniser ? Si c'est fait trop souvent, les perfs s'effondrent.
Si c'est fait trop peu souvent, le thread de rendu va donner l'impression de "saccader", parce que les dernières modifs ne lui parviendront pas assez souvent.
Je vais essayer mais j'ai bien peur que ça ne fonctionne pas correctement.
Au passage le thread de calcul est souvent occupé quasiment à 100% (parce qu'il n'arrive pas à effectuer les simulations aussi vite qu'on lui demande)
Merci encore... J'essaie et je vous tiens au courant.
A+,
Marsh Posté le 15-04-2012 à 19:34:52
En général, une fois par frame. Tu fais ta logique et ton rendu en parallèle. Une fois que les deux ont fini, tu as un point de synchro. Pas de lock, donc pas de raison que les perfs s'effondrent et tu veux juste, au rendu d'une frame, que toute la frame de logique précédente soit terminée. Sinon, ca sert à rien de faire le rendu.
Marsh Posté le 15-04-2012 à 20:37:02
Le soucis, c'est que souvent "la logique" fonctionne à 100%, donc avec un nombre de frames par seconde très proche de 0. Attendre que ce thread ait terminé sa frame est impossible.
Juste une question :
Comment mon code précédent peut crasher ? Je fais des simples accès lecture / écriture en parallèle, et encore, je ne lis même pas ce qui est en cours d'écriture (je ne lis pas tout le tableau comme indiqué dans les commentaires, j'ignore l'élément le plus récent).
Merci
Marsh Posté le 15-04-2012 à 20:43:50
Ben là comme ça je dirais que les valeurs aux adresses foirent (l.9/10), teste les pointeurs
Y'a pas un std::out_of_range qui est renvoyé ?
Marsh Posté le 15-04-2012 à 22:04:42
T'es sur que les adresses données par tes tableaux sont bon ?
Marsh Posté le 15-04-2012 à 22:22:14
Ah en fait je suis pas sur pour l'exception... si jentoure la partie qui plante d'un try/catch pour attraper un std::out of range j'ai rien mais d'après le débugger j'ai une exception non gérée.
Par contre le test suivant montre que les pointeurs ne sont pas NULL en tout cas :
Code :
|
Marsh Posté le 15-04-2012 à 22:59:21
Je suis pas sûr que le break arrête le code dans le if
Marsh Posté le 16-04-2012 à 01:01:08
Oui mais le break casse aussi le code dans le if ?
Marsh Posté le 16-04-2012 à 10:51:38
Qu'entends tu par là ?
Je place cette ligne en début de boucle, si la condition est vérifiée ce qui vient après n'est pas évalué ?!
Sinon je ne comprends pas : rien ne devrait toucher mon objet vec durant l'exécution du code, et pourtant ça plante. Je ne comprends pas comment utiliser la concurrency runtime peut arranger les choses.
J'aimerais comprendre ça avant d'essayer d'appliquer la solution de theshockwave, sinon je risque de mal m'y prendre.
Marsh Posté le 16-04-2012 à 15:27:22
Non rien en fait, j'pensais que le break arrêtait la boucle après la fin du if, mais ça casse aussi le if
Marsh Posté le 16-04-2012 à 19:55:11
ITM a écrit : |
Salut, il faudrait passer par du triple buffering :
- trois zones mémoires work / back / front
- le thread de calcul bosse dans work, quand il a fini il swape les buffers work et back
- quand le thread de calcul veut un nouvel état il swape les buffers back et front puis effectue le rendu avec les données du buffer front
- seuls les swap doivent être synchronisés.
j'ai codé ça rapide pour tester :
Code :
|
la classe LapinMutex encapsule juste un mutex Windows.
le thread Physic remplit un vector de 5 entiers, le thread principal affiche le contenu toutes les secondes.
Marsh Posté le 16-04-2012 à 20:32:08
C'est un peu ce que je proposais expliqué d'une autre manière, mais vraisemblablement, son problème est de trouver une granularité plus fine pour appliquer ce principe, et ca, on peut difficilement le faire à sa place sans connaître plus précisément le fonctionnement de son "work thread".
Marsh Posté le 16-04-2012 à 20:38:12
J'avais sauté ton message.
En effet, dans le cas présent les données de rendu et les données de calcul ont l'air un peu mélangées, ce qui pose problème.
Marsh Posté le 16-04-2012 à 21:07:49
Bonsoir,
merci beaucoup pour vos réponses (et pour ton exemple !)
Je détaille un peu la situation.
J'ai deux threads en parallèles.
- Le thread principal, qui gère les entrées/sorties, et qui est le thread de rendu. Il tourne en boucle à un FPS maxi de 40.
- Le "work thread" comme tu l'appelles, qui effectue les calculs. Il doit effectuer un certain nombre de calculs par seconde. En fait il s'agit de simuler le mouvement de planètes (cf ma signature) et la vitesse de simulation est déterminée par l'utilisateur.
(Ex : 80 jours simulés par seconde).
Il y a un pas pour les calculs (plus ce pas est faible, plus la précision est grande.). Si ce pas est très petit, le programme doit effectuer un trop grand nombre de calculs par seconde, il ne peut plus effectuer les calculs aussi vite que la vitesse de simulation le requiert. Dans ce cas il n'y a pas de temps mort, le "work thread"... travaille à fond.
Niveau code, j'ai essayé de simplifier au maximum :
Thread de rendu :
Code :
|
Et la fonction render() appelle entre autres cette portion de code :
Code :
|
Thread de calculs :
Code :
|
Merci encore,
A+
Marsh Posté le 16-04-2012 à 21:30:34
ton approche est étrange ...
* Soit tu fais du temps réel, auquel cas, tu ne vas pas t'amuser à faire des pas de simulation fixes (tu vas prendre un delta de temps qui correspond au temps de calcul/rendu multiplié par un facteur de conversion qui te donnera le nombre de jours à calculer)
* Soit pour une raison de qualité des calculs ou autre, tu fais de la simulation par pas fixe auquel cas, je ne comprends pas trop l'utilité d'avoir un rendu à framerate fixe.
Dans les deux cas, tu veux que ton processeur carbure à plein régime, tu ne veux pas qu'il fasse 80Fps s'il a ce qu'il faut sous le coude pour en faire 200. Que ca te gagne en précision sur ta simulation (parce que les pas sont variables puisques liés au delta T) ou en temps de rendu total (parce que tu as N pas à faire, N étant fixé avant le rendu)
Marsh Posté le 16-04-2012 à 21:40:25
Alors je ne veux pas que le pas soit variable, je veux qu'il soit fixé par l'utilisateur.
Mais je dois être à la ramasse, je ne vois pas où tu veux en venir..
Le thread de calcul doit travailler à fond. Donc je ne pense pas qu'on puisse faire ça autrement qu'en mettant en parallèle rendu et calculs ??
(L'affichage doit rester fluide, d'ailleurs...)
Actuellement le confort d'utilisation est top je trouve : affichage fluide, calculs plutôt rapides et possibilité de les rendre précis sans pénaliser directement l'affichage.
Donc selon toi je me gourre carrément d'approche ?
A+ et merci
Marsh Posté le 20-04-2012 à 12:10:16
Ton affichage, tu le fait avec un VBO, et pas des glVertex.
Pour moi le CPU doit actualiser son vecteur de particules, et quand tu as fini, tu mets à jour le VBO.
Marsh Posté le 20-04-2012 à 19:45:48
bjone a écrit : Ton affichage, tu le fait avec un VBO, et pas des glVertex. |
bon il faut quand même que je conserve l'historique des 5 dernières positions au moins (pour faire certains calculs)
EDIT : quoi qu'il vaille mieux tout conserver en mémoire ? je regarderai ton truc plus en profondeur
A+,
Marsh Posté le 21-04-2012 à 12:46:41
Le mieux ce serait *tout* fait en local au GPU.
Avoir n VBOs, et ping-ponguer entre chaque VBO.
Ton code d'actualisation des particules, il est suffisamment simple (pas de collisions avec des modèles géométriques) pour tourner sur GPU non, t'as quoi.
Enfin si c'est pour 8 planètes, ça sert à rien de se faire chier avec ça, c'est quand tu veux simuler la trajectoire de 100000 particules que tu cherches à tout faire in-situ au GPU.
Marsh Posté le 11-03-2012 à 21:32:00
Bonsoir,
Je rencontre actuellement un problème assez embêtant avec un petit programme multithread (SDL + OpenGL) que je suis en train de créer.
Actuellement j'ai deux threads :
Mon problème : les tableaux sur lesquels travaille le thread de calcul (qui sont en fait des tableaux contenant des positions de points à dessiner) sont lus par une fonction du thread principal.
Or il se peut que lors de l'exécution de cette fonction d'affichage, un élement ait été retiré du tableau par le thread de calcul entre temps, et dans ce cas, ça plante.
J'ai donc pensé faire un systeme de mutex/condition pour que les écritures sur le tableau soient verrouillées pendant l'affichage et vice-versa. Problème : la fonction d'affichage est plutôt lente, et les calculs extrêmement fréquents...
Au final, les calculs sont très ralentis. L'affichage est à peu près fluide (un peu moins) mais les calculs sont beaucoup moins rapides et ne sont plus cohérents (le thread de calcul est sensé calculer par exemple "n" positions par seconde mais il n'y parvient plus).
Auriez vous d'autres idées pour rendre mes tableaux "thread safe", sans empêcher le bon fonctionnement de l'application ?
(si mon post ne vous parait pas très clair, n'hésitez pas à demander des informations supplémentaires)
Merci d'avance !
Message édité par Profil supprimé le 11-03-2012 à 21:35:36