Multithreading: est-ce que je me trompe?


23

Je travaille sur une application qui joue de la musique.

Pendant la lecture, souvent, les choses doivent se produire sur des threads séparés car elles doivent se produire simultanément. Par exemple, les notes d'un besoin d'accord pour être entendues ensemble, de sorte que chacun se voit attribuer son propre fil à jouer dans (Edit pour clarifier:. Appel note.play()gèle le fil jusqu'à ce que la note est fini de jouer, ce qui est la raison pour laquelle j'ai besoin de trois fils séparés pour entendre trois notes en même temps.)

Ce type de comportement crée de nombreux threads lors de la lecture d'un morceau de musique.

Par exemple, considérons un morceau de musique avec une courte mélodie et une courte progression d'accords d'accompagnement. La mélodie entière peut être jouée sur un seul fil, mais la progression a besoin de trois fils pour jouer, puisque chacun de ses accords contient trois notes.

Ainsi, le pseudo-code pour jouer une progression ressemble à ceci:

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            runOnNewThread( func(){ note.play(); } );
}

Donc, en supposant que la progression a 4 accords et que nous la jouons deux fois, nous ouvrons 3 notes * 4 chords * 2 times= 24 threads. Et c'est juste pour y jouer une fois.

En fait, cela fonctionne bien dans la pratique. Je ne remarque aucune latence notable, ni aucun bogue en résultant.

Mais je voulais demander si c'était la bonne pratique, ou si je fais quelque chose de fondamentalement mauvais. Est-il raisonnable de créer autant de threads chaque fois que l'utilisateur appuie sur un bouton? Sinon, comment puis-je procéder différemment?


14
Vous devriez peut-être plutôt envisager de mixer l'audio? Je ne sais pas quel framework vous utilisez mais voici un exemple: wiki.libsdl.org/SDL_MixAudioFormat Ou, vous pouvez utiliser des canaux: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...dépend du modèle de threading de la langue. Les threads utilisés pour le parallélisme sont souvent gérés au niveau du système d'exploitation afin que le système d'exploitation puisse les mapper sur plusieurs cœurs. Ces threads sont coûteux à créer et à basculer. Les threads pour la simultanéité (entrelacement de deux tâches, n'exécutant pas nécessairement les deux simultanément) peuvent être implémentés au niveau de la langue / machine virtuelle et peuvent être rendus extrêmement «bon marché» pour produire et basculer entre vous afin, par exemple, de parler à 10 sockets réseau plus ou moins simultanément, mais vous n'obtiendrez pas nécessairement plus de débit CPU de cette façon.
Doval

3
Cela mis à part, les autres ont certainement raison sur le fait que les threads sont la mauvaise façon de gérer la lecture de plusieurs sons à la fois.
Doval

3
Connaissez-vous bien le fonctionnement des ondes sonores? En règle générale, vous créez un accord en composant les valeurs de deux ondes sonores (spécifiées au même débit binaire) ensemble dans une nouvelle onde sonore. Des vagues complexes peuvent être construites à partir de vagues simples; vous n'avez besoin que d'une seule forme d'onde pour jouer une chanson.
KChaloux

Puisque vous dites que note.play () n'est pas asynchrone, un thread pour chaque note.play () est donc une approche pour jouer plusieurs notes simultanément. À MOINS .., vous pouvez combiner ces notes en une seule que vous jouez ensuite sur un seul thread. Si cela n'est pas possible, alors avec votre approche, vous devrez utiliser un mécanisme pour vous assurer qu'ils restent synchronisés
pnizzle

Réponses:


46

Une hypothèse que vous faites peut ne pas être valide: vous avez besoin (entre autres) que vos threads s'exécutent simultanément. Cela pourrait fonctionner pour 3, mais à un moment donné, le système devra prioriser les threads à exécuter en premier et celui à attendre.

Votre implémentation dépendra finalement de votre API, mais la plupart des API modernes vous permettront de dire à l'avance ce que vous voulez jouer et de prendre soin du timing et de la mise en file d'attente. Si vous deviez coder vous-même une telle API, en ignorant toute API système existante (pourquoi le feriez-vous?!), Une file d'attente d'événements mélangeant vos notes et les jouant à partir d'un seul thread ressemble à une meilleure approche qu'un modèle de thread par note.


36
Ou pour le dire différemment - le système ne garantira pas et ne pourra pas garantir l'ordre, la séquence ou la durée d'un thread une fois démarré.
James Anderson

2
@JamesAnderson, sauf si l'on entreprendrait des efforts massifs de synchronisation, qui finiraient par être presque à la fin quasiment à nouveau.
Mark

Par «API», vous voulez dire la bibliothèque audio que j'utilise?
Aviv Cohn

1
@Prog Oui. Je suis sûr qu'il a quelque chose de plus pratique que note.play ()
ptyx

26

Ne vous fiez pas aux threads qui s'exécutent en mode verrouillé. Chaque système d'exploitation que je connais ne garantit pas que les threads s'exécutent dans le temps de manière cohérente. Cela signifie que si le CPU exécute 10 threads, ils ne reçoivent pas nécessairement le même temps dans une seconde donnée. Ils pourraient rapidement se désynchroniser ou rester parfaitement synchronisés. Telle est la nature des threads: rien n'est garanti, car le comportement de leur exécution est non déterministe .

Pour cette application spécifique, je pense que vous devez avoir un seul thread qui consomme des notes de musique. Avant de pouvoir consommer les notes, un autre processus doit fusionner les instruments, les portées, quoi que ce soit en un seul morceau de musique .

Supposons que vous ayez par exemple trois fils produisant des notes. Je les synchroniserais sur une seule structure de données où ils pourraient tous mettre leur musique. Un autre thread (consommateur) lit les notes combinées et les joue. Il peut y avoir besoin d'un court délai ici pour garantir que le timing du thread ne provoque pas la disparition des notes.

Lecture connexe: problème producteur-consommateur .


1
Merci de répondre. Je ne suis pas sûr à 100% de votre réponse, mais je voulais m'assurer que vous comprenez pourquoi j'ai besoin de 3 threads pour jouer 3 notes en même temps: c'est parce que l'appel note.play()gèle le thread jusqu'à ce que la note soit jouée. Donc, pour que je puisse play()3 notes en même temps, j'ai besoin de 3 threads différents pour ce faire. Votre solution répond-elle à ma situation?
Aviv Cohn

Non, et cela ne ressort pas clairement de la question. N'y a-t-il aucun moyen pour un thread de jouer un accord?

4

Une approche classique pour ce problème musical serait d'utiliser exactement deux threads. Un, un thread de priorité inférieure gérerait l'interface utilisateur ou le code générateur de son comme

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            otherthread.startPlaying(note);
}

(notez le concept de ne démarrer la note que de manière asynchrone et de continuer sans attendre qu'elle se termine)

et le deuxième thread en temps réel examinerait systématiquement toutes les notes, sons, échantillons, etc. qui devraient être joués maintenant; les mélanger et produire la forme d'onde finale. Cette partie peut être (et est souvent) tirée d'une bibliothèque tierce polie.

Ce thread serait souvent très sensible à la "famine" des ressources - si tout traitement de sortie audio réel est bloqué plus longtemps que la sortie mise en mémoire tampon de votre carte son, il provoquera des artefacts audibles tels que des interruptions ou des sauts. Si vous avez 24 threads qui sortent directement de l'audio, alors vous avez une chance beaucoup plus élevée que l'un d'eux bégaie à un moment donné. Ceci est souvent considéré comme inacceptable, car les humains sont plutôt sensibles aux pépins audio (beaucoup plus qu'aux artefacts visuels) et remarquent même un bégaiement mineur.


1
OP dit que l'API ne peut jouer qu'une seule note à la fois
Mooing Duck

4
@MooingDuck si l'API peut se mélanger, alors elle doit se mélanger; si l'OP dit que l'API ne peut pas mélanger, la solution consiste à mélanger votre code et à ce que l'autre thread exécute my_mixed_notes_from_whole_chord_progression.play () via l'API.
Peteris

1

Eh bien, oui, vous faites quelque chose de mal.

La première chose est que la création de Threads coûte cher. Il a beaucoup plus de frais généraux que d'appeler simplement une fonction.

Donc, ce que vous devez faire, si vous avez besoin de plusieurs threads pour ce travail, c'est de recycler les threads.

Il me semble que vous avez un thread principal qui fait la planification des autres threads. Ainsi, le fil principal mène à travers la musique et commence de nouveaux fils chaque fois qu'il y a une nouvelle note à jouer. Ainsi, au lieu de laisser les threads mourir et de les redémarrer, mieux vaut garder les autres threads en vie dans une boucle de sommeil, où ils vérifient toutes les x millisecondes (ou nanosecondes) s'il y a une nouvelle note à jouer et sinon dorment. Le thread principal ne démarre alors pas de nouveaux threads mais indique au pool de threads existant de jouer des notes. Ce n'est que s'il n'y a pas assez de threads dans le pool qu'il peut créer de nouveaux threads.

Le suivant est la synchronisation. Pratiquement aucun système multithreading moderne ne garantit réellement que tous les threads sont exécutés en même temps. Si vous avez plus de threads et de processus en cours d'exécution sur la machine que de cœurs (ce qui est généralement le cas), les threads n'obtiennent pas 100% du temps CPU. Ils doivent partager le CPU. Cela signifie que chaque thread obtient une petite quantité de temps CPU, puis après son partage, le thread suivant obtient le CPU pendant une courte période. Le système ne garantit pas que votre thread obtient le même temps CPU que les autres threads. Cela signifie qu'un thread peut attendre la fin d'un autre et être ainsi retardé.

Vous devriez plutôt jeter un œil s'il n'est pas possible de jouer plusieurs notes sur un même fil, afin que le fil prépare toutes les notes et ne donne ensuite que la commande "start".

Si vous devez le faire avec des threads, réutilisez au moins les threads. Ensuite, vous n'avez pas besoin d'avoir autant de threads qu'il y a de notes dans toute la pièce, mais seulement besoin d'autant de threads que le nombre maximum de notes jouées en même temps.


"[Est-il] possible de jouer plusieurs notes sur un même fil, afin que le fil prépare toutes les notes et ne donne ensuite que la commande" start "." - C'était aussi ma première pensée. Je me demande parfois si le bombardement de commentaires sur le multithreading (par exemple programmers.stackexchange.com/questions/43321/… ) ne trompe pas beaucoup de programmeurs lors de la conception. Je suis sceptique vis-à-vis de tout gros processus initial qui finit par poser le besoin d'un tas de fils. Je conseillerais de rechercher attentivement une solution à un seul thread.
user1172763

1

La réponse pragmatique est que si cela fonctionne pour votre application et répond à vos exigences actuelles, vous ne vous trompez pas :) Cependant, si vous voulez dire "est-ce une solution évolutive, efficace et réutilisable", la réponse est non. La conception fait de nombreuses hypothèses sur le comportement des threads, qui peuvent ou non être vraies dans différentes situations (mélodies plus longues, notes plus simultanées, matériel différent, etc.).

Comme alternative, envisagez d'utiliser une boucle de synchronisation et un pool de threads . La boucle de synchronisation s'exécute dans son propre thread et vérifie continuellement si une note doit être jouée. Pour ce faire, il compare l'heure du système avec l'heure de début de la mélodie. Le timing de chaque note peut normalement être calculé très facilement à partir du tempo de la mélodie et de la séquence des notes. Comme une nouvelle note doit être jouée, empruntez un thread à un pool de threads et appelez la play()fonction sur la note. La boucle de synchronisation peut fonctionner de différentes manières, mais la plus simple est une boucle continue avec de courtes périodes de sommeil (probablement entre les accords) pour minimiser l'utilisation du processeur.

Un avantage de cette conception est que le nombre de threads ne dépassera pas le nombre maximum de notes simultanées + 1 (la boucle de synchronisation). De plus, la boucle de synchronisation vous protège contre les glissements de synchronisation qui pourraient être causés par la latence du thread. Troisièmement, le tempo de la mélodie n'est pas fixe et peut être modifié en modifiant le calcul des timings.

En passant, je suis d'accord avec d'autres commentateurs sur le fait que la fonction note.play()est une API très médiocre avec laquelle travailler. Toute API sonore raisonnable vous permettrait de mélanger et de planifier des notes d'une manière beaucoup plus flexible. Cela dit, parfois nous devons vivre avec ce que nous avons :)


0

Cela me semble être une implémentation simple, en supposant que c'est l'API que vous devez utiliser . D'autres réponses expliquent pourquoi ce n'est pas une très bonne API, donc je ne parle pas de cela plus, je suppose simplement que c'est ce avec quoi vous devez vivre. Votre approche utilisera un grand nombre de threads, mais sur un PC moderne, cela ne devrait pas vous inquiéter, tant que le nombre de threads est dans les dizaines.

Une chose que vous devriez faire, si possible (comme jouer à partir d'un fichier plutôt que de l'utilisateur frappant le clavier), est d'ajouter une certaine latence. Donc, vous démarrez un thread, il dort jusqu'à l'heure précise de l'horloge système et commence à jouer une note au bon moment (note, parfois le sommeil peut être interrompu plus tôt, alors vérifiez l'horloge et dormez plus si nécessaire). Bien qu'il n'y ait aucune garantie que le système d'exploitation continuera le thread exactement à la fin de votre sommeil (à moins que vous n'utilisiez un système d'exploitation en temps réel), il est très probable qu'il soit beaucoup plus précis que si vous venez de démarrer le thread et de commencer à jouer sans vérifier le timing .

Ensuite, l'étape suivante, qui complique un peu les choses mais pas trop, et vous permettra de réduire la latence mentionnée ci-dessus, utilisez un pool de threads. Autrement dit, lorsqu'un thread termine une note, il ne se ferme pas, mais attend à la place qu'une nouvelle note soit jouée. Et lorsque vous demandez à commencer à jouer une note, vous essayez d'abord de prendre un thread gratuit dans le pool et d'ajouter un nouveau thread uniquement si nécessaire. Cela nécessitera, bien sûr, une communication simple entre les threads au lieu de votre approche actuelle de tir et d'oubli.

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.