Multithreading: estou fazendo errado?


23

Estou trabalhando em um aplicativo que toca música.

Durante a reprodução, muitas vezes as coisas precisam acontecer em threads separados porque precisam acontecer simultaneamente. Por exemplo, as notas de uma necessidade de acordes para ser ouvido em conjunto, de modo que cada um é atribuído o seu próprio segmento a ser jogado em (Edit para esclarecer:. Chamada note.play()congela o fio até que a nota é feito jogando, e é por isso que eu preciso de três tópicos separados para ouvir três notas ao mesmo tempo.)

Esse tipo de comportamento cria muitos tópicos durante a reprodução de uma peça musical.

Por exemplo, considere uma peça musical com uma melodia curta e uma progressão curta de acordes. A melodia inteira pode ser tocada em um único segmento, mas a progressão precisa de três segmentos para tocar, pois cada um de seus acordes contém três notas.

Portanto, o pseudo-código para reproduzir uma progressão fica assim:

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

Então, supondo que a progressão tenha 4 acordes e a toque duas vezes, do que estamos abrindo 3 notes * 4 chords * 2 times= 24 threads. E isso é apenas para jogar uma vez.

Na verdade, funciona bem na prática. Não percebo latência perceptível ou bugs resultantes disso.

Mas eu queria perguntar se essa é uma prática correta ou se estou fazendo algo fundamentalmente errado. É razoável criar tantos threads cada vez que o usuário aperta um botão? Caso contrário, como posso fazer isso de maneira diferente?


14
Talvez você deva considerar misturar o áudio? Não sei qual estrutura você está usando, mas aqui está um exemplo: wiki.libsdl.org/SDL_MixAudioFormat Ou você pode usar os canais: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...depende do modelo de encadeamento do idioma. Os threads usados ​​para paralelismo são geralmente tratados no nível do SO, para que o SO possa mapeá-los para vários núcleos. Tais threads são caros para criar e alternar entre eles. Os encadeamentos para simultaneidade (intercalar duas tarefas, não necessariamente executando as duas simultaneamente) podem ser implementados no nível do idioma / VM e podem ser extremamente "baratos" para produzir e alternar entre eles, para que você possa, digamos, falar com 10 soquetes de rede mais ou menos simultaneamente, mas você não necessariamente terá mais taxa de transferência de CPU dessa maneira.
Doval

3
Além disso, os outros estão definitivamente certos sobre os tópicos serem a maneira errada de lidar com a reprodução de vários sons ao mesmo tempo.
Doval

3
Você tem muita familiaridade com o funcionamento das ondas sonoras? Normalmente, você cria um acorde compondo os valores de duas ondas sonoras (especificadas na mesma taxa de bits) juntas em uma nova onda sonora. Ondas complexas podem ser construídas a partir de ondas simples; você só precisa de uma única forma de onda para tocar uma música.
KChaloux

Como você diz que note.play () não é assíncrono, um encadeamento para cada note.play () é, portanto, uma maneira de reproduzir várias notas simultaneamente. A menos que .., você é capaz de combinar essas notas em uma que você toca em um único segmento. Se isso não for possível, então com a sua abordagem que você vai ter que usar algum mecanismo para garantir que eles permaneçam em sincronia
pnizzle

Respostas:


46

Uma suposição que você está fazendo pode não ser válida: você exige (entre outras coisas) que seus encadeamentos sejam executados simultaneamente. Pode funcionar para 3, mas em algum momento o sistema precisará priorizar quais threads executar primeiro e qual esperar.

Sua implementação dependerá, em última análise, da sua API, mas a maioria das APIs modernas permitirá que você diga com antecedência o que deseja reproduzir e cuide do tempo e da fila de espera. Se você codificasse essa API, ignorando qualquer API do sistema existente (por que você faria ?!), uma fila de eventos misturando suas anotações e reproduzindo-as em um único encadeamento parece uma abordagem melhor do que um modelo de encadeamento por nota.


36
Ou, para dizer de maneira diferente - o sistema não garante e não pode garantir a ordem, sequência ou duração de qualquer thread, uma vez iniciado.
James Anderson

2
@JamesAnderson, exceto se alguém empreender esforços massivos em sincronização, que acabariam sendo quase squetialmente no final novamente.
Mark

Por 'API', você quer dizer a biblioteca de áudio que estou usando?
Aviv Cohn

1
@Prog Sim. Tenho certeza que tem algo mais conveniente que note.play ()
ptyx

26

Não confie nos threads em execução no lockstep. Todo sistema operacional que conheço não garante que os encadeamentos sejam executados no tempo consistentes entre si. Isso significa que, se a CPU executar 10 threads, ela não receberá necessariamente o mesmo tempo em um segundo. Eles poderiam sair rapidamente da sincronia ou permanecer perfeitamente sincronizados. Essa é a natureza dos encadeamentos: nada é garantido, pois o comportamento de sua execução é não determinístico .

Para esta aplicação específica, acredito que você precisa ter um único segmento que consuma notas musicais. Antes que possa consumir as notas, algum outro processo precisa mesclar instrumentos, equipes, o que for, em uma única peça de música .

Vamos supor que você tenha, por exemplo, três threads produzindo notas. Eu os sincronizaria em uma única estrutura de dados, onde todos eles poderiam colocar suas músicas. Outro segmento (consumidor) lê as notas combinadas e as toca. Pode haver um pequeno atraso aqui para garantir que o tempo do encadeamento não faça com que as notas sejam perdidas.

Leitura relacionada: problema produtor-consumidor .


1
Obrigado por responder. Não tenho 100% de certeza de que entendi sua resposta, mas queria ter certeza de que você entende por que preciso de 3 threads para tocar 3 notas ao mesmo tempo: é porque a chamada note.play()congela o thread até que a nota termine de tocar. Então, para eu poder fazer play()3 anotações ao mesmo tempo, preciso de 3 threads diferentes para fazer isso. Sua solução resolve minha situação?
Aviv Cohn

Não, e isso não ficou claro na pergunta. Não há como um fio tocar um acorde?

4

Uma abordagem clássica para esse problema de música seria usar exatamente dois threads. Primeiro, um segmento de prioridade mais baixa manipularia a interface do usuário ou o código de geração de som, como

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

(observe o conceito de apenas iniciar a nota de forma assíncrona e continuar sem esperar que ela termine)

e o segundo tópico em tempo real consistentemente analisaria todas as notas, sons, amostras etc. que deveriam estar tocando agora; misture-os e produza a forma de onda final. Esta parte pode ser (e geralmente é) retirada de uma biblioteca de terceiros polida.

Esse encadeamento costuma ser muito sensível à "falta de recursos" - se algum processamento de saída de som real for bloqueado por mais tempo que a saída em buffer da placa de som, ele causará artefatos audíveis, como interrupções ou pops. Se você tiver 24 threads que produzem áudio diretamente, terá uma chance muito maior de que um deles gagueje em algum momento. Isso geralmente é considerado inaceitável, pois os seres humanos são bastante sensíveis a falhas de áudio (muito mais do que a artefatos visuais) e notam gagueira ainda menor.


1
OP diz API só pode tocar uma nota de cada vez
Mooing Duck

4
@MooingDuck se a API puder se misturar, ela deverá se misturar; se o OP disser que a API não pode ser combinada, a solução é misturar seu código e fazer com que outro thread execute my_mixed_notes_from_whole_chord_progression.play () através da API.
Peteris

1

Bem, sim, você está fazendo algo errado.

A primeira coisa é que a criação de threads é cara. Tem muito mais sobrecarga do que apenas chamar uma função.

Portanto, o que você deve fazer, se precisar de vários threads para este trabalho, é reciclar os threads.

Como me parece, você tem um thread principal que faz o agendamento dos outros threads. Portanto, o fio principal passa pela música e inicia novos fios sempre que houver uma nova nota a ser tocada. Portanto, em vez de deixar os threads morrerem e reiniciá-los, é melhor manter os outros threads ativos em um ciclo de suspensão, onde eles verificam cada x milissegundos (ou nanossegundos) se há uma nova nota a ser tocada e, de outra forma, dormem. O encadeamento principal não inicia novos encadeamentos, mas diz ao pool de encadeamentos existente para tocar notas. Somente se não houver threads suficientes no pool, ele poderá criar novos threads.

O próximo é a sincronização. Quase nenhum sistema moderno de multithreading garante que todos os threads sejam executados ao mesmo tempo. Se você tiver mais threads e processos em execução na máquina do que núcleos (o que geralmente ocorre), os threads não receberão 100% do tempo da CPU. Eles têm que compartilhar a CPU. Isso significa que cada encadeamento recebe uma pequena quantidade de tempo da CPU e, depois que foi compartilhado, o próximo encadeamento obtém a CPU por um curto período de tempo. O sistema não garante que seu thread obtenha o mesmo tempo de CPU que os outros threads. Isso significa que um encadeamento pode estar esperando o término de outro e, portanto, atrasar.

Você deve dar uma olhada se não for possível tocar várias notas em um thread, para que o thread prepare todas as notas e depois forneça apenas o comando "start".

Se você precisar fazer isso com threads, pelo menos reutilize as threads. Então você não precisa ter tantos threads quanto notas na peça inteira, mas precisa de tantos threads quanto o número máximo de notas tocadas ao mesmo tempo.


"[É] possível tocar várias notas em um thread, para que o thread prepare todas as notas e depois dê apenas o comando" start "." - Este foi o meu primeiro pensamento também. Às vezes me pergunto se ser bombardeado com comentários sobre multithreading (por exemplo: programmers.stackexchange.com/questions/43321/… ) não leva muitos programadores a se perderem durante o design. Eu sou cético em relação a qualquer processo grande e inicial que acabe postando a necessidade de um monte de threads. Aconselho procurar atentamente por uma solução de thread único.
user1172763

1

A resposta pragmática é que, se funciona para o seu aplicativo e atende aos seus requisitos atuais, você não está fazendo errado :) No entanto, se você quer dizer "essa é uma solução escalável, eficiente e reutilizável", então a resposta é não. O design faz muitas suposições sobre como os threads se comportarão, o que pode ou não ser verdadeiro em diferentes situações (melodias mais longas, notas mais simultâneas, hardware diferente etc.).

Como alternativa, considere usar um loop de temporização e um pool de threads . O loop de tempo é executado em seu próprio segmento e verifica continuamente se uma nota precisa ser tocada. Isso é feito comparando a hora do sistema com a hora em que a melodia foi iniciada. O tempo de cada nota normalmente pode ser calculado com muita facilidade a partir do andamento da melodia e da sequência das notas. Como uma nova nota precisa ser tocada, peça emprestado um encadeamento de um pool de encadeamentos e chame a play()função na anotação. O loop de temporização pode funcionar de várias maneiras, mas o mais simples é um loop contínuo com sonos curtos (provavelmente entre acordes) para minimizar o uso da CPU.

Uma vantagem desse design é que o número de encadeamentos não excederá o número máximo de notas simultâneas + 1 (o loop de temporização). Além disso, o loop de tempo protege contra derrapagens nos tempos que podem ser causados ​​pela latência do encadeamento. Em terceiro lugar, o andamento da melodia não é fixo e pode ser alterado alterando o cálculo dos tempos.

Como um aparte, concordo com outros comentadores de que a função note.play()é uma API muito ruim para se trabalhar. Qualquer API de som razoável permitiria misturar e agendar notas de uma maneira muito mais flexível. Dito isto, às vezes temos que conviver com o que temos :)


0

Parece-me bem como uma implementação simples, assumindo que é a API que você deve usar . Outras respostas abrangem por que essa API não é muito boa, então não falo mais sobre isso, apenas presumo que é com isso que você deve conviver. Sua abordagem utilizará um grande número de threads, mas em um PC moderno que não deve ser motivo de preocupação, desde que a contagem de threads seja de dezenas.

Uma coisa que você deve fazer, se possível (como reproduzir um arquivo em vez de o usuário pressionar o teclado), é adicionar alguma latência. Assim, você inicia um encadeamento, ele dorme até a hora específica do relógio do sistema e começa a tocar uma nota na hora certa (note que às vezes o sono pode ser interrompido mais cedo, verifique o relógio e durma mais, se necessário). Embora não haja garantia de que o sistema operacional continue o encadeamento exatamente quando seu sono terminar (a menos que você use um sistema operacional em tempo real), é muito provável que seja muito mais preciso do que se você apenas iniciasse o encadeamento e iniciasse a reprodução sem verificar o tempo .

Em seguida, o próximo passo, que complica um pouco as coisas, mas não muito, e permite reduzir a latência mencionada acima, use um pool de threads. Ou seja, quando um segmento termina uma nota, ele não sai, mas aguarda a reprodução de uma nova nota. E quando você solicita o início da reprodução de uma nota, primeiro tente retirar um segmento gratuito do pool e adicionar um novo segmento apenas se necessário. Isso exigirá alguma comunicação inter-thread simples, é claro, em vez da sua abordagem atual de acionar e esquecer.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.