Estoy trabajando en una aplicación que reproduce música.
Durante la reproducción, a menudo las cosas deben suceder en subprocesos separados porque deben suceder simultáneamente. Por ejemplo, las notas de una necesidad acorde a ser escuchados juntos, por lo que cada uno se le asigna su propio hilo que se jugará en (Editar para aclarar:. Llamadas note.play()
congela el hilo hasta que la nota es juego de hacer, y es por eso que necesito tres hilos separados para que se escuchen tres notas al mismo tiempo).
Este tipo de comportamiento crea muchos hilos durante la reproducción de una pieza musical.
Por ejemplo, considere una pieza musical con una melodía corta y una breve progresión de acordes. La melodía completa se puede tocar en un solo hilo, pero la progresión necesita tres hilos para jugar, ya que cada uno de sus acordes contiene tres notas.
Entonces el pseudocódigo para reproducir una progresión se ve así:
void playProgression(Progression prog){
for(Chord chord : prog)
for(Note note : chord)
runOnNewThread( func(){ note.play(); } );
}
Asumiendo que la progresión tiene 4 acordes, y lo tocamos dos veces, entonces estamos abriendo 3 notes * 4 chords * 2 times
= 24 hilos. Y esto es solo por jugarlo una vez.
En realidad, funciona bien en la práctica. No noto ninguna latencia notable o errores resultantes de esto.
Pero quería preguntar si esta es una práctica correcta, o si estoy haciendo algo fundamentalmente incorrecto. ¿Es razonable crear tantos hilos cada vez que el usuario presiona un botón? Si no, ¿cómo puedo hacerlo de manera diferente?
fuente
Is it reasonable to create so many threads...
depende del modelo de subprocesos del lenguaje. Los subprocesos utilizados para el paralelismo a menudo se manejan a nivel del sistema operativo para que el sistema operativo pueda asignarlos a múltiples núcleos. Tales hilos son caros de crear y cambiar. Los subprocesos para la concurrencia (entrelazando dos tareas, no necesariamente ejecutando ambas simultáneamente) se pueden implementar a nivel de lenguaje / VM y se pueden hacer extremadamente "baratos" para producir y cambiar entre ellos para que pueda, por ejemplo, hablar con 10 enchufes de red más o menos simultáneamente, pero no necesariamente obtendrá más rendimiento de CPU de esa manera.Respuestas:
Una suposición que está haciendo podría no ser válida: requiere (entre otras cosas) que sus hilos se ejecuten simultáneamente. Podría funcionar para 3, pero en algún momento el sistema necesitará priorizar qué hilos ejecutar primero y cuál esperar.
En última instancia, su implementación dependerá de su API, pero la mayoría de las API modernas le permitirán saber de antemano lo que desea jugar y se encargarán del tiempo y las colas. Si tuviera que codificar una API de este tipo, ignorando cualquier API del sistema existente (¿por qué lo haría?), Una cola de eventos que mezcla sus notas y las reproduce desde un solo hilo parece un mejor enfoque que un modelo de hilo por nota.
fuente
No confíe en los hilos que se ejecutan en bloque. Todos los sistemas operativos que conozco no garantizan que los subprocesos se ejecuten a tiempo de manera coherente entre sí. Esto significa que si la CPU ejecuta 10 subprocesos, no necesariamente reciben el mismo tiempo en un segundo dado. Podrían salir rápidamente de la sincronización, o podrían permanecer perfectamente sincronizados. Esa es la naturaleza de los hilos: nada está garantizado, ya que el comportamiento de su ejecución no es determinista .
Para esta aplicación específica, creo que necesita tener un solo hilo que consuma notas musicales. Antes de que pueda consumir las notas, algún otro proceso necesita fusionar instrumentos, personal, lo que sea en una sola pieza musical .
Supongamos que tiene, por ejemplo, tres hilos que producen notas. Los sincronizaría en una sola estructura de datos donde todos pueden poner su música. Otro hilo (consumidor) lee las notas combinadas y las reproduce. Es posible que deba haber una pequeña demora aquí para garantizar que la sincronización del hilo no provoque que falten notas.
Lectura relacionada: problema productor-consumidor .
fuente
note.play()
congela el hilo hasta que la nota termine de reproducirse. Entonces, para poder tenerplay()
3 notas al mismo tiempo, necesito 3 hilos diferentes para hacer esto. ¿Su solución aborda mi situación?Un enfoque clásico para este problema musical sería utilizar exactamente dos hilos. Uno, un subproceso de menor prioridad manejaría la IU o el código de generación de sonido como
(tenga en cuenta el concepto de solo comenzar la nota de forma asincrónica y continuar sin esperar a que termine)
y el segundo hilo en tiempo real miraría consistentemente todas las notas, sonidos, muestras, etc. que deberían reproducirse ahora; mezclarlos y generar la forma de onda final. Esta parte puede ser (y a menudo es) tomada de una biblioteca de terceros pulida.
Ese hilo a menudo sería muy sensible a la "falta de recursos" de los recursos: si cualquier procesamiento de salida de sonido real se bloquea por más tiempo que la salida almacenada en la tarjeta de sonido, causará artefactos audibles como interrupciones o pops. Si tiene 24 subprocesos que emiten audio directamente, entonces tiene una probabilidad mucho mayor de que uno de ellos tartamudee en algún momento. Esto a menudo se considera inaceptable, ya que los humanos son bastante sensibles a las fallas de audio (mucho más que a los artefactos visuales) y notan incluso tartamudeos menores.
fuente
Bueno, sí, estás haciendo algo mal.
Lo primero es que crear hilos es costoso. Tiene mucho más gastos generales que simplemente llamar a una función.
Entonces, lo que debe hacer, si necesita varios subprocesos para este trabajo, es reciclarlos.
Según me parece, tiene un hilo principal que hace la programación de los otros hilos. Entonces, el hilo principal conduce a través de la música y comienza nuevos hilos cada vez que hay una nueva nota para reproducir. Entonces, en lugar de dejar que los subprocesos mueran y reiniciarlos, mejor mantenga los otros subprocesos vivos en un ciclo de suspensión, donde verifican cada x milisegundos (o nanosegundos) si hay una nueva nota para reproducir y de lo contrario duermen. El hilo principal no inicia nuevos hilos, pero le dice al grupo de hilos existente que toque notas. Solo si no hay suficientes hilos en el grupo, puede crear nuevos hilos.
El siguiente es la sincronización. Casi ningún sistema moderno de subprocesos múltiples garantiza realmente que todos los subprocesos se ejecuten al mismo tiempo. Si tiene más subprocesos y procesos que se ejecutan en la máquina que núcleos (que es principalmente el caso), entonces los subprocesos no obtienen el 100% del tiempo de CPU. Tienen que compartir la CPU. Esto significa que cada subproceso obtiene una pequeña cantidad de tiempo de CPU y luego, después de haberlo compartido, el siguiente subproceso obtiene la CPU por un corto tiempo. El sistema no garantiza que su hilo tenga el mismo tiempo de CPU que los otros hilos. Esto significa que un hilo podría estar esperando que otro termine y, por lo tanto, retrasarse.
Debería echar un vistazo si no es posible reproducir varias notas en un hilo, de modo que el hilo prepare todas las notas y luego solo dé el comando "inicio".
Si necesita hacerlo con hilos, al menos reutilice los hilos. Entonces no necesita tener tantos hilos como notas en toda la pieza, pero solo necesita tantos hilos como el número máximo de notas tocadas al mismo tiempo.
fuente
La respuesta pragmática es que si funciona para su aplicación y cumple con sus requisitos actuales, entonces no lo está haciendo mal :) Sin embargo si quiere decir "¿es esta una solución escalable, eficiente y reutilizable", entonces la respuesta es no. El diseño hace muchas suposiciones sobre cómo se comportarán los hilos, lo que puede ser cierto o no en diferentes situaciones (melodías más largas, más notas simultáneas, hardware diferente, etc.).
Como alternativa, considere usar un bucle de sincronización y un grupo de subprocesos . El bucle de tiempo se ejecuta en su propio hilo y comprueba continuamente si es necesario tocar una nota. Lo hace comparando la hora del sistema con la hora en que se inició la melodía. El tiempo de cada nota normalmente se puede calcular muy fácilmente a partir del tempo de la melodía y la secuencia de notas. Como se debe tocar una nueva nota, tome prestado un hilo de un grupo de hilos y llame al
play()
función en la nota. El bucle de sincronización puede funcionar de varias maneras, pero el más simple es un bucle continuo con breves interrupciones (probablemente entre acordes) para minimizar el uso de la CPU.Una ventaja de este diseño es que el número de hilos no excederá el número máximo de notas simultáneas + 1 (el ciclo de sincronización). Además, el bucle de tiempo lo protege contra deslizamientos en los tiempos que podrían ser causados por la latencia del hilo. En tercer lugar, el tempo de la melodía no es fijo y puede cambiarse alterando el cálculo de los tiempos.
Como comentario aparte, estoy de acuerdo con otros comentaristas en que la función
note.play()
es una API muy pobre para trabajar. Cualquier API de sonido razonable le permitiría mezclar y programar notas de una manera mucho más flexible. Dicho esto, a veces tenemos que vivir con lo que tenemos :)fuente
Me parece bien como una implementación simple, suponiendo que esa es la API que tienes que usar . Otras respuestas cubren por qué esta no es una API muy buena, así que no estoy hablando más de eso, solo asumo que es con lo que tienes que vivir. Su enfoque utilizará una gran cantidad de subprocesos, pero en una PC moderna no debería ser una preocupación, siempre y cuando el recuento de subprocesos sea decenas.
Una cosa que debe hacer, si es factible (como jugar desde un archivo en lugar de que el usuario toque el teclado), es agregar algo de latencia. Entonces, comienza un hilo, duerme hasta la hora específica del reloj del sistema y comienza a tocar una nota en el momento correcto (nota, a veces el sueño puede interrumpirse temprano, así que revise el reloj y duerma más si es necesario). Si bien no hay garantía de que el sistema operativo continuará el hilo exactamente cuando termine su suspensión (a menos que use un sistema operativo en tiempo real), es muy probable que sea mucho más preciso que si solo inicia el hilo y comienza a jugar sin verificar el tiempo .
Luego, el siguiente paso, que complica un poco las cosas pero no demasiado, y le permitirá reducir la latencia mencionada anteriormente, utilice un grupo de subprocesos. Es decir, cuando un hilo termina una nota, no sale, sino que espera a que suene una nueva nota. Y cuando solicite comenzar a tocar una nota, primero intente tomar un hilo libre del grupo y agregar un nuevo hilo solo si es necesario. Esto requerirá una comunicación simple entre hilos, por supuesto, en lugar de su enfoque actual de disparar y olvidar.
fuente