Multithreading: sto sbagliando?

23

Sto lavorando a un'applicazione che riproduce musica.

Durante la riproduzione, spesso le cose devono succedere su thread separati perché devono accadere simultaneamente. Ad esempio, le note di un accordo devono essere ascoltate insieme, quindi ad ognuna viene assegnata una propria discussione da riprodurre. (Modifica per chiarire: chiamando note.play() congela il thread fino a quando la nota è terminata, ed è per questo che Ho bisogno di tre thread separati per avere tre note ascoltate allo stesso tempo.)

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

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

Quindi lo pseudo-codice per la riproduzione di una progressione assomiglia a questo:

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

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

In realtà, funziona in pratica. Non ho notato alcuna latenza evidente o bug risultanti 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 farlo in modo diverso?

    
posta Aviv Cohn 26.08.2014 - 02:28
fonte

6 risposte

46

Un'ipotesi che stai facendo potrebbe non essere valida: devi (tra le altre cose) che i tuoi thread vengano eseguiti simultaneamente. Potrebbe funzionare per 3, ma ad un certo punto il sistema avrà la necessità di stabilire la priorità dei thread da eseguire per primi, e quale attendere.

La tua implementazione dipenderà in ultima analisi dalla tua API, ma la maggior parte delle API moderne ti permetteranno di dire in anticipo cosa vuoi giocare e di occuparti dei tempi e della messa in coda. Se dovessi codificare una tale API tu stesso, ignorando qualsiasi API di sistema esistente (perché dovresti ?!), una coda di eventi che mescola le tue note e riprodurle da un singolo thread sembra un approccio migliore rispetto a un modello di thread per nota.

    
risposta data 26.08.2014 - 02:52
fonte
26

Non fare affidamento su thread in esecuzione a serrature. Ogni sistema operativo che conosco non garantisce che i thread vengano eseguiti nel tempo coerenti tra loro. Ciò significa che se la CPU esegue 10 thread, non necessariamente ricevono uguale tempo in un dato secondo. Potrebbero rapidamente uscire fuori sincrono, o potrebbero rimanere perfettamente sincronizzati. Questa è la natura dei thread: nulla è garantito, poiché il loro comportamento è non deterministico .

Per questa specifica applicazione, credo che sia necessario un thread singolo che consumi note musicali. Prima che possa consumare le note, qualche altro processo deve unire strumenti, staff e qualsiasi cosa in un singolo brano .

Supponiamo che tu abbia per es. tre thread che producono note. Li sincronizzerei su un'unica struttura dati in cui tutti possono mettere la propria musica. Un altro thread (utente) legge le note combinate e le riproduce. Potrebbe essere necessario un breve ritardo per garantire che il tempo del thread non causi la mancanza di note.

Lettura correlata: problema produttore-consumatore .

    
risposta data 26.08.2014 - 04:44
fonte
4

Un approccio classico per questo problema musicale sarebbe quello di utilizzare esattamente due thread. Uno, un thread con priorità più bassa gestiva l'interfaccia utente o il codice di generazione del suono come

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

(nota il concetto di solo iniziando la nota in modo asincrono e procedendo senza aspettare che finisca)

e il secondo thread in tempo reale guarderebbe costantemente tutti 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 è) presa da una libreria di terze parti lucidata.

Quella discussione sarebbe spesso molto sensibile alla "fame" di risorse - se l'effettiva elaborazione dell'uscita audio è bloccata più a lungo dell'output bufferizzato della scheda audio, causerà artefatti udibili come interruzioni o pop. Se hai 24 thread che emettono direttamente audio, hai una possibilità molto più alta che uno di loro possa balbettare ad un certo punto. Questo è spesso considerato inaccettabile, in quanto gli umani sono piuttosto sensibili ai problemi audio (molto più che agli artefatti visivi) e notano anche balbuzie minori.

    
risposta data 26.08.2014 - 22:27
fonte
1

Bene, sì, stai facendo qualcosa di sbagliato.

La prima cosa è che creare thread è costoso. Ha un sovraccarico molto maggiore rispetto al semplice richiamo di una funzione.

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

Come mi sembra tu abbia un thread principale che fa la programmazione degli altri thread. Quindi il filo principale conduce attraverso la musica e inizia nuovi thread ogni volta che c'è una nuova nota da suonare. Quindi, invece di lasciare morire i thread e riavviarli, è meglio mantenere vivi gli altri thread in un ciclo di sospensione, dove controllano ogni x millisecondi (o nanosecondi) se c'è una nuova nota da riprodurre e altrimenti si dorme. Il thread principale quindi non avvia nuovi thread ma dice al pool di thread esistente di riprodurre note. Solo se non ci sono abbastanza thread nel pool, può creare nuovi thread.

Il prossimo è la sincronizzazione. Quasi nessun moderno sistema di multithreading garantisce effettivamente che tutti i thread vengano eseguiti contemporaneamente. Se sul computer sono presenti più thread e processi in esecuzione rispetto ai core (che è in genere il caso), i thread non ricevono il 100% del tempo della CPU. Devono condividere la CPU. Ciò significa che ogni thread riceve una piccola quantità di tempo CPU e quindi, dopo averlo condiviso, il thread successivo ottiene la CPU per un breve periodo. Il sistema non garantisce che il tuo thread abbia la stessa CPU degli altri thread. Ciò significa che un thread potrebbe attendere il completamento di un altro e quindi essere ritardato.

Dovresti 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 hai bisogno di farlo con i thread, riutilizza almeno i thread. Quindi non è necessario avere tanti thread quante sono le note dell'intero brano, ma solo il numero massimo di thread del numero massimo di note suonate contemporaneamente.

    
risposta data 26.08.2014 - 13:10
fonte
1

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

In alternativa, considera l'utilizzo di un loop di temporizzazione e un thread pool . Il ciclo temporale viene eseguito nella propria thread e controlla continuamente se è necessario riprodurre una nota. Lo fa confrontando il tempo di sistema con il tempo in cui è stata avviata la melodia. Il tempo di ciascuna nota può normalmente essere calcolato molto facilmente dal tempo della melodia e dalla sequenza di note. Poiché è necessario riprodurre una nuova nota, prendere in prestito una discussione da un pool di thread e chiamare la funzione play() sulla nota. Il loop temporale può funzionare in vari modi, ma il più semplice è un loop continuo con sleep brevi (probabilmente tra gli 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 ciclo temporale). Anche il ciclo di temporizzazione ti protegge dagli slittamenti nei tempi che potrebbero essere causati dalla latenza dei thread. Terzo, il tempo della melodia non è fisso e può essere modificato alterando il calcolo dei tempi.

Per inciso, sono d'accordo con altri commentatori sul fatto che la funzione note.play() sia un'API molto scarsa con cui lavorare. Qualsiasi ragionevole API audio ti consentirebbe di combinare e programmare le note in un modo molto più flessibile. Detto questo, a volte dobbiamo convivere con ciò che abbiamo:)

    
risposta data 27.08.2014 - 12:53
fonte
0

Mi sembra una semplice implementazione, supponendo che sia l'API che devi usare . Altre risposte coprono il motivo per cui questa API non è molto buona, quindi non ne parlo più, presumo solo che è ciò con cui devi convivere. Il tuo approccio utilizzerà un numero elevato di thread, ma su un PC moderno che non dovrebbe preoccuparti, a patto che il numero dei thread sia nelle dozzine.

Una cosa che dovresti fare, se possibile (come suonare da un file anziché dall'utente che colpisce la tastiera), è aggiungere una certa latenza. Quindi si avvia una discussione, si dorme fino all'orario specifico dell'orologio di sistema e inizia a suonare una nota al momento giusto (nota, a volte il sonno può essere interrotto presto, quindi controllare l'orologio e dormire di più se necessario). Anche se non vi è alcuna garanzia che il sistema operativo continui 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 di se si avvia il thread e si inizia a giocare senza controllare i tempi .

Quindi il prossimo passo, che complica le cose un po 'ma non troppo, e ti permetterà di ridurre la latenza sopra menzionata, usa un pool di thread. Cioè, quando un thread finisce una nota, non esce, ma attende invece che venga suonata una nuova nota. E quando richiedi di iniziare a suonare una nota, per prima cosa provi a prendere un thread libero dal pool e aggiungi un nuovo thread solo se necessario. Ciò richiede alcune semplici comunicazioni tra thread, ovviamente, al posto del tuo attuale approccio fire-and-forget.

    
risposta data 27.08.2014 - 13:16
fonte

Leggi altre domande sui tag