La differenza tra queste scelte è nell'affinità delle assegnazioni di attività ai thread .
Come spiego in una domanda precedente , è interamente una tua scelta implementare questa affinità o meno. Ci sono modi per implementare il multithreading senza l'affinità delle attività sui thread.
Il pattern Consumer-Consumer è adatto se:
- La pipeline del flusso di dati è lineare, senza fork e join nel flusso di dati
- Il produttore ha un output
- Il consumatore ha un input
- Tutte le fasi intermedie hanno esattamente un input e un output
- I dati sono sequenziali
- Se ogni pezzo di dati viene contrassegnato con un numero crescente in serie, tutte le fasi vedranno i dati che arrivano con gli stessi valori incrementali in serie.
- Ogni fase intermedia deve generare esattamente un elemento di output per ogni oggetto ricevuto.
- È possibile attenuare questa limitazione, come ho spiegato nei commenti altrove . In generale, le code FIFO non hanno questa limitazione.
- Se massimizzare l'utilizzo della CPU non è l'obiettivo (*).
- In generale, mentre lo schema Producer-Consumer può utilizzare più di un core, non utilizza più core rispetto al numero di stadi di cui dispone.
- Se esiste un collo di bottiglia (ovvero, c'è uno stage che impiega il tempo di elaborazione più lungo rispetto ad altri stadi), il throughput di questa fase determinerà il throughput dell'intero sistema.
Per massimizzare il throughput (*), generalmente si proverà:
- Rompere la pipeline in fasi a grana fine.
- Permetti di eseguire alcune fasi intermedie in parallelo.
- L'analogia della CPU sarebbe la duplicazione di unità di esecuzione nell'architettura superscalare.
- Nel software, questo significa che alcune fasi scelte con cura verranno eseguite da più thread.
- Questi thread verranno alimentati da una singola coda FIFO, elaboreranno ciascun dato separatamente e quindi invieranno il loro output in una coda di riordino (*) protetta da thread singolo.
- Concentrarsi sull'ottimizzazione delle fasi del collo di bottiglia. Esempi:
- Ottimizzazione dell'algoritmo.
- Micro-ottimizzazione, come la programmazione SIMD.
- Offloading su dispositivi di elaborazione specializzati, come GPU o FPGA.
(*) Massimizzare la velocità effettiva - il numero di frame video elaborati al secondo - è l'obiettivo finale. Non massimizzare l'utilizzo della CPU.
Alcune ottimizzazioni come i miglioramenti dell'algoritmo diminuiranno l'utilizzo della CPU di quel livello, aumentando nel contempo l'efficienza generale in modo che, nel complesso, lo stesso risultato possa essere calcolato con un numero inferiore di istruzioni CPU totali eseguite.
(*) La coda di riordino è necessaria perché quando vengono elaborati due pezzi di dati, taggati [0]
e [1]
, a volte l'output per [1]
termina prima del tempo. Per conservare la proprietà di ordinamento della pipeline, l'output per [1]
deve essere trattenuto fino a quando l'output per [0]
è pronto.
Se queste modifiche non sono sufficienti per massimizzare il throughput al livello desiderato, si potrebbe anche allontanarsi dal modello produttore-consumatore a pipeline singola e passare a un flusso di dati o a un framework di dati.
In un flusso di dati o un quadro di dati più generale:
- Il grafico del flusso di dati non è lineare. È possibile utilizzare qualsiasi grafico aciclico diretto (DAG) delle fasi di elaborazione.
- Le attività non hanno affinità con i thread.
- Tutti gli stadi diventano senza memoria (senza stato) per impostazione predefinita (non sono autorizzati a portare avanti lo stato), a meno che non siano modellati esplicitamente con un percorso di dipendenza dei dati.
- Tutti gli stadi possono essere eseguiti simultaneamente in qualsiasi numero (molteplicità) per impostazione predefinita, a meno che non siano esplicitamente vincolati con un percorso di dipendenza del controllo.
Disclaimer: parte della terminologia utilizzata qui può essere allentata o errata. Le correzioni sono benvenute.