Multithreading: ¿lo estoy haciendo mal?

23

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?

Aviv Cohn
fuente
14
¿Quizás deberías considerar mezclar el audio en su lugar? No sé qué marco está utilizando, pero aquí hay un ejemplo: wiki.libsdl.org/SDL_MixAudioFormat O, podría utilizar los canales: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind
55
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.
Doval
3
Aparte de eso, los otros definitivamente tienen razón acerca de que los hilos son la forma incorrecta de manejar la reproducción de múltiples sonidos a la vez.
Doval
3
¿Tiene mucha familiaridad con el funcionamiento de las ondas sonoras? Por lo general, crea un acorde al componer los valores de dos ondas de sonido (especificadas en la misma velocidad de bits) en una nueva onda de sonido. Las ondas complejas se pueden construir a partir de simples; solo necesitas una forma de onda para reproducir una canción.
KChaloux
Como usted dice que note.play () no es asíncrono, un hilo para cada note.play () es, por lo tanto, un enfoque para tocar varias notas simultáneamente. A MENOS ..., puede combinar esas notas en una que luego toque en un solo hilo. Si eso no es posible, entonces, con su enfoque, tendrá que usar algún mecanismo para asegurarse de que permanezcan sincronizados
pnizzle

Respuestas:

46

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.

ptyx
fuente
36
O para decirlo de otra manera: el sistema no podrá garantizar el orden, la secuencia o la duración de ningún hilo una vez iniciado.
James Anderson
2
@JamesAnderson, excepto si uno emprendiera esfuerzos masivos en sincronización, lo que terminaría siendo casi al final de manera casi escéptica.
Mark
¿Por 'API' te refieres a la biblioteca de audio que estoy usando?
Aviv Cohn
1
@Prog Sí. Estoy bastante seguro de que tiene algo más conveniente que note.play ()
ptyx
26

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
1
Gracias por responder. No estoy 100% seguro de entender tu respuesta, pero quería asegurarme de que entiendas por qué necesito 3 hilos para tocar 3 notas al mismo tiempo: es porque llamar note.play()congela el hilo hasta que la nota termine de reproducirse. Entonces, para poder tener play()3 notas al mismo tiempo, necesito 3 hilos diferentes para hacer esto. ¿Su solución aborda mi situación?
Aviv Cohn
No, y eso no estaba claro en la pregunta. ¿No hay forma de que un hilo toque un acorde?
4

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

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

(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.

Pedro es
fuente
1
OP dice que API solo puede tocar una nota a la vez
Mooing Duck
44
@MooingDuck si API puede mezclar, entonces debería mezclar; si el OP dice que la API no se puede mezclar, entonces la solución es mezclar su código y hacer que otro hilo ejecute my_mixed_notes_from_whole_chord_progression.play () a través de la API.
Peteris
1

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.

Dakkaron
fuente
"[Es posible] reproducir varias notas en un hilo, de modo que el hilo prepare todas las notas y luego solo dé el comando" inicio ". - Ese fue mi primer pensamiento también. A veces me pregunto si ser bombardeado con comentarios sobre subprocesos múltiples (por ejemplo, programmers.stackexchange.com/questions/43321/… ) no lleva a muchos programadores por mal camino durante el diseño. Soy escéptico de cualquier proceso grande y directo que termine planteando la necesidad de un montón de hilos. Aconsejaría buscar atentamente una solución de un solo hilo.
user1172763
1

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 alplay() 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 :)

John
fuente
0

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.

Hyde
fuente