Je crée un moteur de jeu 2D simple et je veux mettre à jour et rendre les sprites dans différents threads, pour savoir comment cela se fait.
J'ai besoin de synchroniser le fil de mise à jour et celui de rendu. Actuellement, j'utilise deux drapeaux atomiques. Le flux de travail ressemble à ceci:
Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done
Dans cette configuration, je limite le FPS du fil de rendu au FPS du fil de mise à jour. De plus, j'utilise sleep()
pour limiter à la fois le rendu et la mise à jour du FPS du thread à 60, de sorte que les deux fonctions d'attente n'attendront pas beaucoup de temps.
Le problème est:
L'utilisation moyenne du processeur est d'environ 0,1%. Parfois, cela peut aller jusqu'à 25% (dans un PC quadricœur). Cela signifie qu'un thread attend l'autre car la fonction wait est une boucle while avec une fonction test et set, et une boucle while utilise toutes vos ressources CPU.
Ma première question est: existe-t-il une autre façon de synchroniser les deux threads? J'ai remarqué que std::mutex::lock
n'utilisez pas le CPU pendant qu'il attend pour verrouiller une ressource, ce n'est donc pas une boucle while. Comment ça marche? Je ne peux pas l'utiliser std::mutex
car je devrai les verrouiller dans un thread et les déverrouiller dans un autre thread.
L'autre question est; comme le programme tourne toujours à 60 FPS, pourquoi son utilisation du processeur passe-t-elle parfois à 25%, ce qui signifie que l'une des deux attentes attend beaucoup? (les deux threads sont tous deux limités à 60 images par seconde, donc ils n'auront idéalement pas besoin de beaucoup de synchronisation).
Edit: Merci pour toutes les réponses. Je veux d'abord dire que je ne démarre pas un nouveau thread à chaque image pour le rendu. Je démarre la boucle de mise à jour et de rendu au début. Je pense que le multithreading peut gagner du temps: j'ai les fonctions suivantes: FastAlg () et Alg (). Alg () est à la fois mon obj de mise à jour et mon obj de rendu et Fastalg () est ma "file d'attente de rendu d'envoi vers" renderer "". En un seul fil:
Alg() //update
FastAgl()
Alg() //render
En deux fils:
Alg() //update while Alg() //render last frame
FastAlg()
Alors peut-être que le multithreading peut vous faire gagner du temps. (en fait, dans une application mathématique simple, c'est le cas, où alg est un long algorithme et fastalg un plus rapide)
Je sais que le sommeil n'est pas une bonne idée, même si je n'ai jamais eu de problèmes. Est-ce que ce sera mieux?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Mais ce sera une boucle while infinie qui utilisera tout le CPU. Puis-je utiliser le mode veille (un nombre <15) pour limiter l'utilisation? De cette façon, il fonctionnera à, par exemple, 100 images par seconde, et la fonction de mise à jour sera appelée seulement 60 fois par seconde.
Pour synchroniser les deux threads, j'utiliserai waitforsingleobject avec createSemaphore afin de pouvoir verrouiller et déverrouiller dans différents threads (sans utiliser de boucle while), n'est-ce pas?
Réponses:
Pour un moteur 2D simple avec des sprites, une approche à un seul thread est parfaitement bonne. Mais comme vous voulez apprendre à faire du multithreading, vous devez apprendre à le faire correctement.
Ne pas
sleep
pour contrôler la fréquence d'images. Jamais. Si quelqu'un vous le dit, frappez-le.Tout d'abord, tous les moniteurs ne fonctionnent pas à 60 Hz. Deuxièmement, deux minuteries coïncidant au même rythme et fonctionnant côte à côte finiront toujours par se désynchroniser (déposez deux balles de pingpong sur une table de la même hauteur et écoutez). Troisièmement, par sa conception , il
sleep
n'est ni précis ni fiable. La granularité peut être aussi mauvaise que 15,6 ms (en fait, la valeur par défaut sur Windows [1] ), et un cadre n'est que de 16,6 ms à 60 images par seconde, ce qui ne laisse que 1 ms pour tout le reste. De plus, il est difficile d'obtenir 16,6 pour être un multiple de 15,6 ... En outre, il est autorisé (et parfois!) De revenir uniquement après 30, 50 ou 100 ms, ou encore plus longtemps.sleep
std::mutex
pour notifier un autre thread. Ce n'est pas pour ça.Faire
sleep
[2] . En outre, une minuterie récurrente prend correctement en compte le temps (y compris le temps qui s'écoule entre les deux), tandis que le sommeil pendant 16,6 ms (ou 16,6 ms moins measure_time_elapsed) ne le fait pas.std::mutex
à un seul thread d'accéder à une ressource à la fois ("mutuellement exclu") et de se conformer à la sémantique bizarre destd::condition_variable
.std::condition_variable
pour bloquer un autre thread jusqu'à ce qu'une condition soit remplie. La sémantique destd::condition_variable
ce mutex supplémentaire est certes assez étrange et tordue (principalement pour des raisons historiques héritées des threads POSIX), mais une variable de condition est la primitive correcte à utiliser pour ce que vous voulez.Si vous trouvez
std::condition_variable
trop bizarre pour être à l'aise avec cela, vous pouvez également utiliser simplement un événement Windows (légèrement plus lent) à la place ou, si vous êtes courageux, créez votre propre événement simple autour de NtKeyedEvents (implique des choses effrayantes de bas niveau). Comme vous utilisez DirectX, vous êtes déjà lié à Windows de toute façon, donc la perte de portabilité ne devrait pas être un gros problème.[1] Oui, vous pouvez définir le taux de l'ordonnanceur à 1 ms, mais cela est mal vu car il provoque beaucoup plus de changements de contexte et consomme beaucoup plus d'énergie (dans un monde où de plus en plus d'appareils sont des appareils mobiles). Ce n'est pas non plus une solution car cela ne rend toujours pas le sommeil plus fiable.
[2] Une minuterie augmentera la priorité du thread, ce qui lui permettra d'interrompre un autre thread de priorité égale au milieu du quantum et d'être programmé en premier, ce qui est un comportement quasi-RT. Ce n'est bien sûr pas vrai RT, mais ça s'en rapproche. Se réveiller du sommeil signifie simplement que le fil est prêt à être programmé à un moment donné, quand cela est possible.
la source
Je ne suis pas sûr de ce que vous voulez réaliser en limitant le FPS de la mise à jour et du rendu à 60. Si vous les limitez à la même valeur, vous auriez pu les mettre dans le même thread.
Le but lors de la séparation de la mise à jour et du rendu dans différents threads est d'avoir les deux «presque» indépendants l'un de l'autre, afin que le GPU puisse rendre 500 FPS et que la logique de mise à jour continue à 60 FPS. Vous n'obtenez pas un gain de performances très élevé en procédant ainsi.
Mais vous avez dit que vous vouliez simplement savoir comment cela fonctionne, et ça va. En C ++, un mutex est un objet spécial qui est utilisé pour verrouiller l'accès à certaines ressources pour d'autres threads. En d'autres termes, vous utilisez un mutex pour rendre les données sensibles accessibles par un seul thread à la fois. Pour ce faire, c'est assez simple:
Source: http://en.cppreference.com/w/cpp/thread/mutex
EDIT : assurez-vous que votre mutex est à l'échelle de la classe ou du fichier, comme dans le lien donné, sinon chaque thread créera son propre mutex et vous n'obtiendrez rien.
Le premier thread pour verrouiller le mutex aura accès au code à l'intérieur. Si un deuxième thread essaie d'appeler la fonction lock (), il se bloquera jusqu'à ce que le premier thread le déverrouille. Un mutex est donc une fonction de blocage, contrairement à une boucle while. Les fonctions de blocage ne mettront pas de stress sur le CPU.
la source
std::lock_guard
ou similaire, non.lock()
/.unlock()
. RAII n'est pas seulement pour la gestion de la mémoire!