Multithreading: mache ich das falsch?

23

Ich arbeite an einer Anwendung, die Musik spielt.

Während der Wiedergabe müssen häufig Dinge in separaten Threads geschehen, da sie gleichzeitig geschehen müssen. Zum Beispiel, um die Noten eines Akkords Notwendigkeit zusammen gehört werden, so jeder seines eigenen Thread zugeordnet wird gespielt werden (Edit zu klären. Rufenden note.play()den Faden gefriert , bis die Notiz fertig spielen ist, und das ist , warum ich drei brauchen separate Themen, um drei Noten gleichzeitig zu hören.)

Diese Art von Verhalten erzeugt während der Wiedergabe eines Musikstücks viele Threads.

Betrachten Sie beispielsweise ein Musikstück mit einer kurzen Melodie und einer kurzen Akkordfolge. Die gesamte Melodie kann auf einem einzelnen Thread gespielt werden, für den Verlauf sind jedoch drei Threads erforderlich, da jeder seiner Akkorde drei Noten enthält.

Der Pseudocode zum Abspielen einer Progression sieht also so aus:

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

Angenommen, die Progression hat 4 Akkorde und wir spielen sie zweimal, dann eröffnen wir 3 notes * 4 chords * 2 times= 24 Threads. Und das nur, um es einmal zu spielen.

Eigentlich funktioniert es in der Praxis gut. Ich bemerke keine merkliche Latenz oder Fehler, die daraus resultieren.

Aber ich wollte fragen, ob dies die richtige Praxis ist oder ob ich etwas grundlegend Falsches tue. Ist es sinnvoll, jedes Mal so viele Threads zu erstellen, wenn der Benutzer einen Knopf drückt? Wenn nicht, wie kann ich das anders machen?

Aviv Cohn
quelle
14
Vielleicht sollten Sie stattdessen das Audio mischen? Ich weiß nicht, welches Framework Sie verwenden, aber hier ist ein Beispiel: wiki.libsdl.org/SDL_MixAudioFormat Oder Sie könnten Kanäle verwenden: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind
5
Is it reasonable to create so many threads...hängt vom Threading-Modell der Sprache ab. Threads, die für die Parallelität verwendet werden, werden häufig auf Betriebssystemebene verarbeitet, damit das Betriebssystem sie mehreren Kernen zuordnen kann. Solche Threads sind teuer zu erstellen und zwischen ihnen zu wechseln. Threads für die Parallelität (Verschachtelung von zwei Tasks, die nicht unbedingt beide gleichzeitig ausführen müssen) können auf Sprach- / VM-Ebene implementiert werden und sind extrem "billig" zu produzieren und zu wechseln, sodass Sie beispielsweise mehr oder weniger mit 10 Netzwerk-Sockets sprechen können gleichzeitig, aber auf diese Weise erhalten Sie nicht unbedingt mehr CPU-Durchsatz.
Doval
3
Abgesehen davon haben die anderen definitiv Recht damit, dass Threads die falsche Art sind, mehrere Sounds gleichzeitig abzuspielen.
Doval
3
Kennen Sie sich gut mit der Funktionsweise von Schallwellen aus? Normalerweise erstellen Sie einen Akkord, indem Sie die Werte zweier Schallwellen (mit derselben Bitrate angegeben) zu einer neuen Schallwelle zusammensetzen. Komplexe Wellen können aus einfachen Wellen aufgebaut werden. Sie benötigen immer nur eine einzige Wellenform, um ein Lied abzuspielen.
KChaloux
Da Sie sagen, dass note.play () nicht asynchron ist, ist ein Thread für jede note.play () daher ein Ansatz zum gleichzeitigen Spielen mehrerer Noten. UNLESS .. können Sie diese Noten zu einer kombinieren, die Sie dann auf einem einzelnen Thread spielen. Wenn das nicht möglich ist, dann mit Ihrem Ansatz sind Sie gehen zu müssen , einen Mechanismus verwenden , um sicherzustellen , bleiben sie synchron
pnizzle

Antworten:

46

Eine Annahme, die Sie machen, ist möglicherweise nicht gültig: Sie benötigen (unter anderem), dass Ihre Threads gleichzeitig ausgeführt werden. Könnte für 3 funktionieren, aber irgendwann muss das System priorisieren, welche Threads zuerst ausgeführt werden sollen und welche warten.

Ihre Implementierung hängt letztendlich von Ihrer API ab, aber die meisten modernen APIs lassen Sie im Voraus wissen, was Sie spielen möchten, und kümmern sich um das Timing und das Anstehen. Wenn Sie eine solche API selbst codieren und eine vorhandene System-API ignorieren (warum ?!), erscheint eine Ereigniswarteschlange, in der Ihre Notizen gemischt und aus einem einzelnen Thread abgespielt werden, als eine bessere Vorgehensweise als ein Thread pro Notenmodell.

ptyx
quelle
36
Oder anders ausgedrückt: Das System übernimmt keine Garantie für die Reihenfolge, Reihenfolge oder Dauer der einmal gestarteten Threads.
James Anderson
2
@JamesAnderson außer wenn man massive Anstrengungen in der Synchronisation unternehmen würde, die am Ende wieder fast ärgerlich werden würden.
Mark
Mit "API" meinen Sie die Audiobibliothek, die ich verwende?
Aviv Cohn
1
@Prog Ja. Ich bin mir ziemlich sicher, dass es etwas Bequemeres gibt als note.play ()
ptyx
26

Verlassen Sie sich nicht auf Threads, die im Gleichschritt ausgeführt werden. Jedes mir bekannte Betriebssystem garantiert nicht, dass Threads zeitlich aufeinander abgestimmt ausgeführt werden. Dies bedeutet, dass die CPU, wenn sie 10 Threads ausführt, nicht unbedingt in jeder Sekunde die gleiche Zeit erhält. Sie könnten schnell aus der Synchronisation geraten oder sie könnten perfekt synchron bleiben. Das ist die Natur von Threads: Es ist nichts garantiert, da das Verhalten ihrer Ausführung nicht deterministisch ist .

Ich glaube, dass Sie für diese spezielle Anwendung einen einzelnen Thread benötigen , der Musiknoten aufnimmt. Bevor die Noten verbraucht werden können, muss ein anderer Prozess Instrumente und Notenzeilen zu einem einzigen Musikstück zusammenführen .

Nehmen wir an, Sie haben zB drei Fäden, die Noten produzieren. Ich würde sie auf einer einzigen Datenstruktur synchronisieren, in der sie alle ihre Musik ablegen können. Ein anderer Thread (Verbraucher) liest die kombinierten Noten und spielt sie ab. Hier kann eine kurze Verzögerung erforderlich sein, um sicherzustellen, dass durch das Thread-Timing keine Notizen verloren gehen.

Verwandte Lektüre: Erzeuger-Verbraucher-Problem .


quelle
1
Danke für die Antwort. Ich bin nicht zu 100% sicher, dass Sie Ihre Antwort verstehen, aber ich wollte sichergehen, dass Sie verstehen, warum ich 3 Threads gleichzeitig zum Spielen von 3 Noten benötige: Der Aufruf note.play()friert den Thread ein, bis die Note abgespielt ist. Damit ich play()3 Noten gleichzeitig lesen kann, benötige ich 3 verschiedene Threads. Geht Ihre Lösung auf meine Situation ein?
Aviv Cohn
Nein, und das wurde aus der Frage nicht deutlich. Kann ein Thread keinen Akkord spielen?
4

Ein klassischer Ansatz für dieses Musikproblem wäre, genau zwei Threads zu verwenden. Erstens würde ein Thread mit niedrigerer Priorität die Benutzeroberfläche oder den Code, der den Ton erzeugt, so behandeln

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

(Beachten Sie das Konzept, die Notiz nur asynchron zu starten und fortzusetzen, ohne darauf zu warten, dass sie beendet wird.)

und der zweite Echtzeit-Thread würde alle Noten, Sounds, Samples usw. konsistent betrachten , die jetzt abgespielt werden sollten; mischen Sie sie und geben Sie die endgültige Wellenform aus. Dieser Teil kann (und wird oft) aus einer polierten Drittanbieter-Bibliothek entnommen werden.

Dieser Thread reagiert häufig sehr empfindlich auf Ressourcenverknappung. Wenn eine tatsächliche Soundausgabeverarbeitung länger als die von Ihrer Soundkarte gepufferte Ausgabe blockiert ist, werden hörbare Artefakte wie Unterbrechungen oder Knackgeräusche verursacht. Wenn Sie 24 Threads haben, die direkt Audio ausgeben, ist die Wahrscheinlichkeit, dass einer von ihnen irgendwann ruckelt, viel höher. Dies wird oft als inakzeptabel angesehen, da Menschen gegenüber Audiostörungen (viel mehr als gegenüber visuellen Artefakten) empfindlich sind und sogar geringfügige Ruckler bemerken.

Peter ist
quelle
1
Laut
4
@MooingDuck Wenn API mischen kann, sollte es mischen; Wenn das OP sagt, dass die API nicht mischen kann, besteht die Lösung darin, Ihren Code zu mischen und diesen anderen Thread my_mixed_notes_from_whole_chord_progression.play () über die API ausführen zu lassen.
Peteris
1

Na ja, du machst etwas falsch.

Das erste ist, dass das Erstellen von Threads teuer ist. Es hat viel mehr Aufwand als nur das Aufrufen einer Funktion.

Wenn Sie also mehrere Threads für diesen Job benötigen, müssen Sie die Threads recyceln.

Wie es für mich aussieht, haben Sie einen Haupt-Thread, der die Planung der anderen Threads übernimmt. Der Haupt-Thread führt also durch die Musik und startet neue Threads, wenn eine neue Note gespielt werden soll. Anstatt die Threads absterben zu lassen und neu zu starten, sollten Sie die anderen Threads in einer Schlafschleife am Leben halten, in der sie alle x Millisekunden (oder Nanosekunden) prüfen, ob eine neue Note zu spielen ist, und ansonsten schlafen. Der Haupt-Thread startet dann keine neuen Threads, sondern weist den vorhandenen Thread-Pool an, Noten abzuspielen. Nur wenn nicht genügend Threads im Pool vorhanden sind, können neue Threads erstellt werden.

Der nächste ist die Synchronisation. Kaum ein modernes Multithreading-System garantiert, dass alle Threads gleichzeitig ausgeführt werden. Wenn auf dem Computer mehr Threads und Prozesse ausgeführt werden als Kerne (was meistens der Fall ist), erhalten die Threads nicht 100% der CPU-Zeit. Sie müssen sich die CPU teilen. Dies bedeutet, dass jeder Thread eine kleine Menge an CPU-Zeit erhält und der nächste Thread nach der Freigabe für kurze Zeit die CPU erhält. Das System garantiert nicht, dass Ihr Thread dieselbe CPU-Zeit wie die anderen Threads erhält. Dies bedeutet, dass ein Thread möglicherweise auf den Abschluss eines anderen Threads wartet und daher verzögert wird.

Sie sollten lieber schauen, ob es nicht möglich ist, mehrere Noten auf einem Thread zu spielen, damit der Thread alle Noten vorbereitet und dann nur den Befehl "Start" gibt.

Wenn Sie dies mit Threads tun müssen, verwenden Sie die Threads zumindest erneut. Dann brauchen Sie nicht so viele Threads, wie das ganze Stück Noten enthält, sondern nur so viele Threads, wie gleichzeitig maximal Noten gespielt werden.

Dakkaron
quelle
"[Ist es] möglich, mehrere Noten auf einem Thread zu spielen, so dass der Thread alle Noten vorbereitet und dann nur den Befehl" Start "gibt." - Das war auch mein erster Gedanke. Ich frage mich manchmal, ob ein Bombardement mit Kommentaren über Multithreading (z. B. programmers.stackexchange.com/questions/43321/… ) nicht viele Programmierer während des Entwurfs in die Irre führt. Ich bin skeptisch gegenüber jedem großen Vor-Ort-Prozess, der letztendlich die Notwendigkeit eines Bündels von Threads unterstellt. Ich würde raten, intensiv nach einer Single-Thread-Lösung zu suchen.
user1172763
1

Die pragmatische Antwort ist , dass , wenn es für Ihre Anwendung funktioniert und erfüllt Ihre aktuellen Anforderungen dann bist du es nicht falsch zu machen :) Aber wenn du meinst „ist dies eine skalierbare, effiziente, wiederverwendbare Lösung“ , dann lautet die Antwort nein. Das Design macht viele Annahmen darüber, wie sich die Threads verhalten, was in verschiedenen Situationen (längere Melodien, mehr gleichzeitige Noten, andere Hardware usw.) wahr sein kann oder nicht.

Als Alternative sollten Sie eine Timing-Schleife und einen Thread- Pool verwenden . Die Timing-Schleife läuft in einem eigenen Thread und prüft ständig, ob eine Note gespielt werden muss. Dazu wird die Systemzeit mit der Zeit verglichen, zu der die Melodie gestartet wurde. Das Timing jeder Note kann normalerweise sehr einfach aus dem Tempo der Melodie und der Reihenfolge der Noten berechnet werden. Wenn eine neue Note gespielt werden muss, leihen Sie sich einen Thread aus einem Thread-Pool aus und rufen Sie die play()Funktion für die Note auf. Die Timing-Schleife kann auf verschiedene Arten funktionieren, aber die einfachste ist eine Endlosschleife mit kurzen Pausen (wahrscheinlich zwischen Akkorden), um die CPU-Auslastung zu minimieren.

Ein Vorteil dieses Entwurfs besteht darin, dass die Anzahl der Threads die maximale Anzahl gleichzeitiger Noten + 1 (die Timing-Schleife) nicht überschreitet. Die Timing-Schleife schützt Sie auch vor Zeitverschiebungen, die durch Thread-Latenz verursacht werden können. Drittens ist das Tempo der Melodie nicht festgelegt und kann durch Ändern der Berechnung der Timings geändert werden.

Im Übrigen stimme ich anderen Kommentatoren zu, dass die Funktion note.play()eine sehr schlechte API ist, mit der man arbeiten kann. Mit jeder vernünftigen Sound-API können Sie Noten viel flexibler mischen und planen. Das heißt, manchmal müssen wir mit dem leben, was wir haben :)

John
quelle
0

Sieht für mich als einfache Implementierung gut aus, vorausgesetzt, dies ist die API, die Sie verwenden müssen . Andere Antworten geben Aufschluss darüber, warum dies keine sehr gute API ist. Ich spreche also nicht weiter darüber. Ich gehe einfach davon aus, dass es das ist, womit Sie leben müssen. Ihr Ansatz wird eine große Anzahl von Threads verwenden, aber auf einem modernen PC sollte dies kein Problem sein, solange die Thread-Anzahl Dutzende beträgt.

Eine Sache, die Sie tun sollten, wenn es machbar ist (wie das Abspielen von einer Datei anstatt von einem Benutzer, der die Tastatur drückt), ist eine gewisse Latenz hinzuzufügen. Wenn Sie einen Thread starten, ruht dieser bis zu einer bestimmten Zeit der Systemuhr und beginnt zur richtigen Zeit mit dem Spielen einer Note. Es gibt zwar keine Garantie dafür, dass das Betriebssystem den Thread genau dann fortsetzt, wenn der Energiesparmodus endet (es sei denn, Sie verwenden ein Echtzeit-Betriebssystem), aber es ist sehr wahrscheinlich viel genauer, als wenn Sie den Thread starten und ohne Überprüfung des Timings mit dem Abspielen beginnen .

Der nächste Schritt, der die Dinge ein wenig kompliziert, aber nicht zu sehr macht und es Ihnen ermöglicht, die oben erwähnte Latenz zu reduzieren, ist die Verwendung eines Thread-Pools. Das heißt, wenn ein Thread eine Note beendet, wird er nicht beendet, sondern wartet darauf, dass eine neue Note abgespielt wird. Wenn Sie anfangen möchten, eine Note zu spielen, versuchen Sie zunächst, einen freien Thread aus dem Pool zu nehmen und nur bei Bedarf einen neuen Thread hinzuzufügen. Dies erfordert natürlich eine einfache Kommunikation zwischen den Threads anstelle Ihres aktuellen Fire-and-Forget-Ansatzes.

hyde
quelle