Multithreading: sto sbagliando?


23

Sto lavorando a un'applicazione che riproduce musica.

Durante la riproduzione, spesso devono succedere cose su thread separati perché devono accadere simultaneamente. Ad esempio, le note di un accordo devono essere ascoltate insieme, quindi a ognuno viene assegnato il proprio thread per essere suonato. (Modifica per chiarire: la chiamata note.play()congela il thread fino a quando la nota non viene suonata, ed è per questo che ho bisogno di tre thread separati per ascoltare tre note contemporaneamente.)

Questo tipo di comportamento crea molti thread durante la riproduzione di un brano musicale.

Ad esempio, considera un brano musicale con una breve melodia e una breve progressione di accordi di accompagnamento. L'intera melodia può essere suonata su un singolo thread, ma la progressione ha bisogno di tre thread per suonare, poiché ciascuno dei suoi accordi contiene tre note.

Quindi lo pseudo-codice per riprodurre una progressione è simile al seguente:

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

Supponendo quindi che la progressione abbia 4 accordi, e la suoniamo due volte, quindi stiamo aprendo 3 notes * 4 chords * 2 times= 24 thread. E questo è solo per giocarci una volta.

In realtà, funziona bene in pratica. Non noto alcuna latenza evidente o bug derivanti da questo.

Ma volevo chiedere se questa è una pratica corretta o se sto facendo qualcosa di fondamentalmente sbagliato. È ragionevole creare così tanti thread ogni volta che l'utente preme un pulsante? In caso contrario, come posso fare diversamente?


14
Forse dovresti considerare di mixare l'audio invece? Non so quale framework stai usando, ma ecco un esempio: wiki.libsdl.org/SDL_MixAudioFormat Oppure potresti usare i canali: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...dipende dal modello di threading della lingua. I thread utilizzati per il parallelismo sono spesso gestiti a livello di sistema operativo in modo che il sistema operativo possa mapparli su più core. Tali thread sono costosi da creare e passare da uno all'altro. I thread per la concorrenza (interleaving di due attività, non necessariamente eseguendo entrambi contemporaneamente) possono essere implementati a livello di lingua / VM e possono essere resi estremamente "economici" per produrre e alternare in modo da poter dire, per esempio, parlare con 10 socket di rete più o meno contemporaneamente, ma non otterrai necessariamente una maggiore velocità della CPU in quel modo.
Doval,

3
A parte questo, gli altri hanno sicuramente ragione sul fatto che i thread siano il modo sbagliato di gestire la riproduzione di più suoni contemporaneamente.
Doval,

3
Hai molta familiarità con il funzionamento delle onde sonore? In genere si crea un accordo componendo i valori di due onde sonore (specificate allo stesso bitrate) insieme in una nuova onda sonora. Le onde complesse possono essere costruite da quelle semplici; hai solo bisogno di una sola forma d'onda per suonare una canzone.
KChaloux,

Poiché dici che note.play () non è asincrono, un thread per ogni note.play () è quindi un approccio per suonare più note contemporaneamente. A MENO CHE, sei in grado di combinare quelle note in una che poi suoni su un singolo thread. Se ciò non è possibile, con il tuo approccio dovrai utilizzare un meccanismo per assicurarti che rimangano sincronizzati
pnizzle,

Risposte:


46

Un presupposto che stai formulando potrebbe non essere valido: è necessario (tra le altre cose) che i thread vengano eseguiti contemporaneamente. Potrebbe funzionare per 3, ma a un certo punto il sistema dovrà stabilire le priorità su quali thread devono essere eseguiti per primi e quali attendere.

L'implementazione dipenderà in definitiva dalla tua API, ma la maggior parte delle API moderne ti permetterà di dire in anticipo cosa vuoi giocare e di occuparti dei tempi e delle code stesse. Se dovessi codificare tale API da solo, ignorando qualsiasi API di sistema esistente (perché dovresti farlo ?!), una coda di eventi che mescola le tue note e le riproduce da un singolo thread sembra un approccio migliore di un thread per modello di nota.


36
O per dirlo diversamente: il sistema non garantirà e non potrà garantire l'ordine, la sequenza o la durata di un thread una volta avviato.
James Anderson,

2
@JamesAnderson tranne se si intraprendessero enormi sforzi di sincronizzazione, che alla fine sarebbero tornati quasi squetialmente.
Segna il

Per "API" intendi la libreria audio che sto usando?
Aviv Cohn,

1
@Prog Sì. Sono abbastanza sicuro che abbia qualcosa di più conveniente di note.play ()
ptyx,

26

Non fare affidamento sui thread in esecuzione in lockstep. Ogni sistema operativo che conosco non garantisce che i thread vengano eseguiti in tempo coerente tra loro. Ciò significa che se la CPU esegue 10 thread, non riceve necessariamente lo stesso tempo in un dato secondo. Potrebbero rapidamente uscire dalla sincronizzazione o potrebbero rimanere perfettamente sincronizzati. Questa è la natura dei thread: nulla è garantito, poiché il comportamento della loro esecuzione non è deterministico .

Per questa specifica applicazione, credo che tu debba avere un singolo thread che consuma le note musicali. Prima che possa consumare le note, alcuni altri processi devono fondere strumenti, personale, qualunque cosa in un unico brano musicale .

Supponiamo che tu abbia ad esempio tre thread che producono note. Li sincronizzerei su un'unica struttura di dati in cui tutti possono mettere la loro musica. Un altro thread (consumatore) legge le note combinate e le suona. Potrebbe essere necessario un breve ritardo in questo caso per garantire che i tempi del thread non causino la scomparsa delle note.

Lettura correlata: problema produttore-consumatore .


1
Grazie per aver risposto. Non sono sicuro al 100% di capire la tua risposta, ma volevo assicurarmi che capissi perché ho bisogno di 3 thread per suonare 3 note contemporaneamente: è perché la chiamata note.play()congela il thread fino a quando la nota non viene suonata. Quindi per poter essere in grado di play()3 note allo stesso tempo, ho bisogno di 3 diversi thread per farlo. La tua soluzione affronta la mia situazione?
Aviv Cohn,

No, e questo non era chiaro dalla domanda. Non c'è modo per un thread di suonare un accordo?

4

Un approccio classico per questo problema musicale sarebbe usare esattamente due thread. Uno, un thread con priorità inferiore gestirà l'interfaccia utente o il suono generando codice come

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

(notare il concetto di avviare la nota solo in modo asincrono e continuare senza attendere che finisca)

e il secondo thread in tempo reale guarderebbe costantemente a tutte le note, i suoni, i campioni, ecc. che dovrebbero essere riprodotti ora; mescolarli e produrre la forma d'onda finale. Questa parte può essere (e spesso viene) presa da una libreria di terze parti perfezionata.

Quel thread sarebbe spesso molto sensibile alla "fame" di risorse - se qualsiasi elaborazione effettiva dell'uscita audio viene bloccata per un tempo superiore all'uscita buffer della scheda audio, causerà artefatti udibili come interruzioni o pop. Se hai 24 thread che stanno emettendo direttamente l'audio, allora hai una probabilità molto più alta che uno di essi possa balbettare ad un certo punto. Questo è spesso considerato inaccettabile, in quanto gli esseri umani sono piuttosto sensibili ai difetti audio (molto più che agli artefatti visivi) e notano anche piccole balbuzie.


1
OP afferma che l'API può suonare solo una nota alla volta
Mooing Duck il

4
@MooingDuck se l'API può mescolare, allora dovrebbe mescolarsi; se l'OP afferma che l'API non è in grado di combinare, la soluzione è quella di mescolare il codice e fare in modo che l'altro thread esegua my_mixed_notes_from_whole_chord_progression.play () attraverso l'API.
Peteris,

1

Bene, sì, stai facendo qualcosa di sbagliato.

La prima cosa è che la creazione di thread è costosa. Ha un sovraccarico molto maggiore rispetto al semplice chiamare una funzione.

Quindi cosa dovresti fare, se hai bisogno di più thread per questo lavoro è riciclare i thread.

A mio avviso, hai un thread principale che esegue la pianificazione degli altri thread. Quindi il thread principale conduce attraverso la musica e inizia nuovi thread ogni volta che c'è una nuova nota da suonare. Quindi, invece di lasciare che i thread muoiano e di riavviarli, è meglio mantenere in vita gli altri thread in un ciclo di sospensione, in cui controllano ogni x millisecondi (o nanosecondi) se c'è una nuova nota da suonare e altrimenti dormire. Il thread principale non avvia quindi nuovi thread ma dice al pool di thread esistente di suonare le note. Solo se non ci sono abbastanza thread nel pool, può creare nuovi thread.

Il prossimo è la sincronizzazione. Quasi nessun moderno sistema multithreading garantisce in realtà che tutti i thread vengano eseguiti contemporaneamente. Se sul computer sono in esecuzione più thread e processi rispetto ai core (che è principalmente il caso), i thread non ottengono il 100% del tempo della CPU. Devono condividere la CPU. Ciò significa che ogni thread ottiene una piccola quantità di tempo della CPU e quindi dopo averla condivisa, il thread successivo ottiene la CPU per un breve periodo. Il sistema non garantisce che il thread abbia lo stesso tempo di CPU degli altri thread. Ciò significa che un thread potrebbe essere in attesa di un altro per finire, e quindi essere ritardato.

Dovresti piuttosto dare un'occhiata se non è possibile suonare più note su un thread, in modo che il thread prepari tutte le note e poi dia solo il comando "start".

Se devi farlo con i thread, almeno riutilizzali. Quindi non hai bisogno di avere tanti thread quante sono le note nell'intero brano, ma hai bisogno solo di tanti thread quanti sono il numero massimo di note suonate contemporaneamente.


"[È possibile] suonare più note su un thread, in modo che il thread prepari tutte le note e poi dia solo il comando" start "." - Anche questo è stato il mio primo pensiero. A volte mi chiedo se essere bombardato da commenti sul multithreading (ad esempio programmers.stackexchange.com/questions/43321/… ) non porti molti programmatori fuori strada durante la progettazione. Sono scettico nei confronti di qualsiasi grande processo iniziale che finisce per postulare la necessità di un sacco di discussioni. Consiglio di cercare attentamente una soluzione a thread singolo.
user1172763,

1

La risposta pragmatica è che se funziona per la tua applicazione e soddisfa i tuoi attuali requisiti, allora non stai sbagliando :) Tuttavia, se intendi "è una soluzione scalabile, efficiente e riutilizzabile", allora la risposta è no. Il design fa molte ipotesi su come si comporteranno i thread che possono o meno essere vere in diverse situazioni (melodie più lunghe, note più simultanee, hardware diverso, ecc.).

In alternativa, considerare l'utilizzo di un loop di temporizzazione e un pool di thread . Il loop di temporizzazione viene eseguito nel proprio thread e verifica continuamente se è necessario suonare una nota. Lo fa confrontando l'ora di sistema con l'ora di inizio della melodia. Il tempo di ciascuna nota può normalmente essere calcolato molto facilmente dal tempo della melodia e dalla sequenza delle note. Poiché è necessario suonare una nuova nota, prendere in prestito un thread da un pool di thread e chiamare la play()funzione sulla nota. Il loop di temporizzazione può funzionare in vari modi, ma il più semplice è un loop continuo con brevi sleep (probabilmente tra accordi) per ridurre al minimo l'utilizzo della CPU.

Un vantaggio di questo design è che il numero di thread non supererà il numero massimo di note simultanee + 1 (il loop di temporizzazione). Inoltre, il circuito di temporizzazione ti protegge dagli slittamenti nei tempi che potrebbero essere causati dalla latenza del thread. In terzo luogo, il tempo della melodia non è fisso e può essere modificato modificando il calcolo dei tempi.

A parte questo, sono d'accordo con altri commentatori sul fatto che la funzione note.play()è un'API molto scadente con cui lavorare. Qualsiasi API audio ragionevole ti consentirebbe di mescolare e pianificare le note in un modo molto più flessibile. Detto questo, a volte dobbiamo convivere con ciò che abbiamo :)


0

Mi sembra una semplice implementazione, supponendo che sia l'API che devi usare . Altre risposte spiegano perché questa non è un'API molto buona, quindi non ne sto parlando più, presumo solo che sia ciò con cui devi convivere. Il tuo approccio utilizzerà un gran numero di thread, ma su un PC moderno ciò non dovrebbe preoccupare, purché il numero di thread sia in dozzine.

Una cosa che dovresti fare, se possibile (come, giocando da un file invece che da un utente che colpisce la tastiera), è aggiungere un po 'di latenza. Quindi si avvia un thread, si dorme fino all'ora specifica dell'orologio di sistema e inizia a suonare una nota al momento giusto (nota, a volte il sonno può essere interrotto in anticipo, quindi controllare l'orologio e dormire di più se necessario). Sebbene non vi sia alcuna garanzia che il sistema operativo continuerà il thread esattamente quando termina il sonno (a meno che non si utilizzi un sistema operativo in tempo reale), è molto probabile che sia molto più preciso rispetto a quando si avvia il thread e si inizia a giocare senza controllare i tempi .

Quindi il prossimo passo, che complica un po 'le cose ma non troppo, e ti permetterà di ridurre la latenza sopra menzionata, usa un pool di thread. Cioè, quando un thread termina una nota, non esce, ma attende invece che venga suonata una nuova nota. E quando richiedi di iniziare a suonare una nota, prova prima a prendere un thread gratuito dal pool e aggiungi un nuovo thread solo se necessario. Ciò richiederà ovviamente una semplice comunicazione inter-thread, invece del tuo attuale approccio di fuoco e dimentica.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.