Quali lezioni hai appreso da un progetto che ha quasi / effettivamente fallito a causa del cattivo multithreading? [chiuso]


11

Quali lezioni hai appreso da un progetto che ha quasi / effettivamente fallito a causa del cattivo multithreading?

A volte, il framework impone un certo modello di threading che rende le cose di un ordine di grandezza più difficili da ottenere.

Per quanto mi riguarda, devo ancora riprendermi dall'ultimo fallimento e sento che è meglio per me non lavorare su nulla che abbia a che fare con il multithreading in quel framework.

Ho scoperto di essere bravo nei problemi di multithreading che hanno un fork / join semplice e in cui i dati viaggiano solo in una direzione (mentre i segnali possono viaggiare in una direzione circolare).

Non sono in grado di gestire la GUI in cui alcuni lavori possono essere eseguiti solo su un thread strettamente serializzato (il "thread principale") e altri lavori possono essere eseguiti solo su qualsiasi thread tranne il thread principale (i "thread di lavoro") e dove dati e messaggi devono viaggiare in tutte le direzioni tra N componenti (un grafico completamente collegato).

Al momento in cui ho lasciato quel progetto per un altro, c'erano problemi di deadlock ovunque. Ho sentito che 2-3 mesi dopo, diversi altri sviluppatori sono riusciti a risolvere tutti i problemi di deadlock, al punto che può essere spedito ai clienti. Non sono mai riuscito a scoprire quel pezzo di conoscenza mancante che mi manca.

Qualcosa riguardo al progetto: il numero di ID messaggio (valori interi che descrivono il significato di un evento che può essere inviato nella coda messaggi di un altro oggetto, indipendentemente dal threading) viene eseguito in diverse migliaia. Anche le stringhe univoche (messaggi utente) arrivano a circa mille.

aggiunto

La migliore analogia che ho ricevuto da un altro team (non correlato ai miei progetti passati o presenti) è stata quella di "mettere i dati in un database". ("Database" che si riferisce alla centralizzazione e agli aggiornamenti atomici.) In una GUI che è frammentata in più viste tutte in esecuzione sullo stesso "thread principale" e tutto il sollevamento pesante non GUI viene eseguito nei singoli thread di lavoro, i dati dell'applicazione dovrebbero essere archiviato in un unico caso che si comporta come un database e lasciare che il "database" gestisca tutti gli "aggiornamenti atomici" che implicano dipendenze di dati non banali. Tutte le altre parti della GUI gestiscono solo il disegno dello schermo e nient'altro. Le parti dell'interfaccia utente potrebbero memorizzare oggetti nella cache e l'utente non noterà se è obsoleto di una frazione di secondo, se progettato correttamente. Questo "database" è anche noto come "il documento" nell'architettura Document-View. Sfortunatamente, la mia app in realtà memorizza tutti i dati nelle viste. Non so perché fosse così.

Collaboratori:

(i partecipanti non devono utilizzare esempi reali / personali. Le lezioni di esempi aneddotici, se giudicato credibile da te stesso, sono anche ben accette).



Penso che essere in grado di "pensare nei fili" sia in qualche modo un talento e meno qualcosa che può essere appreso, per mancanza di una migliore formulazione. Conosco molti sviluppatori che lavorano con sistemi paralleli da molto tempo, ma si bloccano se i dati devono andare in più di una direzione.
dauphic,

Risposte:


13

La mia lezione preferita - molto difficile vinto! - è che in un programma multithread lo scheduler è un suino subdolo che ti odia. Se le cose possono andare male, lo faranno, ma in modo inaspettato. Prendi qualcosa di sbagliato e inseguirai strani heisenbugs (perché qualsiasi strumentazione che aggiungi cambierà i tempi e ti darà un modello di corsa diverso).

L'unico modo sano per risolvere questo problema è di correggere rigorosamente tutta la gestione dei thread in un pezzo di codice così piccolo che lo fa bene e che è molto conservativo nel garantire che i blocchi siano mantenuti correttamente (e con un ordine di acquisizione costante a livello globale) . Il modo più semplice per farlo è quello di non condividere la memoria (o altre risorse) tra i thread ad eccezione della messaggistica che deve essere asincrona; che ti consente di scrivere tutto il resto in uno stile che ignora il thread. (Bonus: il ridimensionamento su più macchine in un cluster è molto più semplice.)


+1 per "non condividere la memoria (o altre risorse) tra i thread ad eccezione della messaggistica che deve essere asincrona;"
Nemanja Trifunovic,

1
L' unico modo? Che dire dei tipi di dati immutabili?
Aaronaught

is that in a multithreaded program the scheduler is a sneaky swine that hates you.- no, non fa esattamente quello che gli hai detto di fare :)
mattnz,

@Aaronaught: i valori globali passati per riferimento, anche se immutabili, richiedono ancora GC globali e che reintroducono un sacco di risorse globali. Essere in grado di utilizzare la gestione della memoria per thread è utile, poiché consente di eliminare un sacco di blocchi globali.
Donal Fellows

Non è che non è possibile passare valori di tipi non di base per riferimento, ma che richiede livelli più elevati di blocco (ad esempio, il "proprietario" detiene un riferimento fino a quando non viene restituito un messaggio, che è facile rovinare durante la manutenzione) o codice complesso nel motore di messaggistica per trasferire la proprietà. Oppure esegui il marshalling di tutto e unmarshal nell'altro thread, che è molto più lento (devi farlo quando vai in un cluster comunque). Tagliare all'inseguimento e non condividere affatto la memoria è più facile.
Donal Fellows

6

Ecco alcune lezioni di base che mi vengono in mente in questo momento (non da progetti falliti ma da problemi reali visti su progetti reali):

  • Cerca di evitare qualsiasi blocco delle chiamate mentre trattieni una risorsa condivisa. Il modello deadlock comune è che il thread prende mutex, effettua una richiamata, blocca callback sullo stesso mutex.
  • Proteggi l'accesso a qualsiasi struttura di dati condivisa con una sezione mutex / critica (o usa quelle senza blocco, ma non inventare la tua!)
  • Non assumere l'atomicità: usa le API atomiche (ad esempio InterlockedIncrement).
  • RTFM relativo alla sicurezza dei thread di librerie, oggetti o API in uso.
  • Approfitta delle primitive di sincronizzazione disponibili, ad esempio eventi, semafori. (Ma fai molta attenzione quando li usi, sai che sei in buono stato - ho visto molti esempi di eventi segnalati nello stato sbagliato in modo che eventi o dati possano andare persi)
  • Supponiamo che i thread possano essere eseguiti contemporaneamente e / o in qualsiasi ordine e che il contesto possa passare da un thread all'altro in qualsiasi momento (a meno che in un sistema operativo che fornisca altre garanzie).

6
  • L'intero progetto GUI dovrebbe essere chiamato solo dal thread principale . Fondamentalmente, non dovresti mettere un singolo (.net) "invoke" nella tua GUI. Il multithreading dovrebbe essere bloccato in progetti separati che gestiscono l'accesso ai dati più lento.

Abbiamo ereditato una parte in cui il progetto GUI utilizza una dozzina di thread. Non sta dando altro che problemi. Deadlock, problemi di corsa, chiamate della GUI cross thread ...


"Progetto" significa "assemblaggio"? Non vedo come la distribuzione delle classi tra gli assembly causerebbe problemi di threading.
Nikie,

Nel mio progetto è davvero un'assemblea. Ma il punto principale è che tutto il codice in quelle cartelle deve essere chiamato dal thread principale, senza eccezioni.
Carra,

Non penso che questa regola sia generalmente applicabile. Sì, non dovresti mai chiamare il codice GUI da un altro thread. Ma come distribuire le classi in cartelle / progetti / assiemi è una decisione indipendente.
Nikie,

1

Java 5 e versioni successive hanno Executor che hanno lo scopo di semplificare la vita nella gestione di programmi multi-threading in stile fork-join.

Usa quelli, rimuoverà molto del dolore.

(e, sì, questo l'ho imparato da un progetto :))


1
Per applicare questa risposta ad altre lingue - quando possibile, utilizzare i framework di elaborazione parallela di alta qualità forniti da quella lingua. (Tuttavia, solo il tempo dirà se un framework è davvero eccezionale e altamente utilizzabile.)
rwong

1

Ho un background in sistemi embedded realtime duri. Non è possibile verificare l'assenza di problemi causati dal multithreading. (A volte puoi confermare la presenza). Il codice deve essere dimostrabilmente corretto. Quindi le migliori pratiche per qualsiasi interazione tra thread.

  • Regola n. 1: BACIO - Se non è necessario un thread, non farne uno. Serializzare il più possibile.
  • Regola n. 2: non infrangere il numero 1.
  • # 3 Se non riesci a dimostrare attraverso la revisione è corretto, non lo è.

+1 per la regola 1. Stavo lavorando a un progetto che inizialmente stava per bloccare fino al completamento di un altro thread, essenzialmente una chiamata di metodo! Fortunatamente, abbiamo deciso di non adottare questo approccio.
Michael K,

# 3 FTW. Meglio passare ore a lottare con i diagrammi dei tempi di blocco o qualunque cosa tu usi per dimostrare che è buono di mesi chiedendoti perché a volte cade a pezzi.

1

Un'analogia da una lezione sul multithreading che ho preso l'anno scorso è stata molto utile. La sincronizzazione dei thread è come un segnale di traffico che protegge un incrocio (dati) dall'essere utilizzato da due auto (thread) contemporaneamente. L'errore che molti sviluppatori fanno è trasformare le luci rosse in gran parte della città per far passare una macchina perché pensano che sia troppo difficile o pericoloso per capire il segnale esatto di cui hanno bisogno. Ciò potrebbe funzionare bene quando il traffico è leggero, ma porterà a un blocco della rete man mano che la tua applicazione cresce.

È qualcosa che già sapevo in teoria, ma dopo quella lezione l'analogia mi ha davvero colpito, e sono rimasto sorpreso da quanto spesso avrei indagato su un problema di threading e avrei trovato una coda gigante, o interrotto la disabilitazione ovunque durante una scrittura su una variabile sono stati usati solo due fili o i mutex sono stati trattenuti a lungo quando potevano essere rifattorizzati per evitarlo del tutto.

In altre parole, alcuni dei peggiori problemi di threading sono causati da un eccessivo tentativo di evitare problemi di threading.


0

Prova a farlo di nuovo.

Almeno per me, ciò che ha creato la differenza è stata la pratica. Dopo aver eseguito il lavoro multi-thread e distribuito un paio di volte, è sufficiente.

Penso che il debug sia davvero ciò che lo rende difficile. Posso eseguire il debug del codice multi thread con VS ma sono davvero in perdita se devo usare gdb. Colpa mia, probabilmente.

Un'altra cosa che sta imparando di più è bloccare le strutture dati libere.

Penso che questa domanda possa essere davvero migliorata se si specifica il framework. I pool di thread .NET e i lavoratori in background sono davvero diversi da QThread, per esempio. Ci sono sempre alcuni gotcha specifici per piattaforma.


Sono interessato a ascoltare storie da qualsiasi framework, perché credo che ci siano cose da imparare da ogni framework, in particolare quelli a cui non sono stato esposto.
rwong

1
i debugger sono in gran parte inutili in un ambiente multi-thread.
Pemdas,

Ho già traccianti di esecuzione multi-thread che mi dicono qual è il problema, ma non mi aiuteranno a risolverlo. Il punto cruciale del mio problema è che "secondo il progetto attuale, non posso trasmettere il messaggio X all'oggetto Y in questo modo (sequenza); deve essere aggiunto a una coda gigante e alla fine verrà elaborato; ma per questo motivo , non è possibile che i messaggi appaiano all'utente al momento giusto: accadrà sempre in modo anacronistico e renderà l'utente molto, molto confuso. Potrebbe anche essere necessario aggiungere barre di avanzamento, pulsanti di annullamento o messaggi di errore in luoghi che non dovrebbero ho quelli ".
rwong

0

Ho imparato che i callback dai moduli di livello inferiore ai moduli di livello superiore sono un enorme male perché causano l'acquisizione di blocchi in un ordine opposto.


i callback non sono cattivi ... il fatto che facciano qualcosa di diverso dalla rottura del thread è probabilmente la radice del male. Sarei altamente sospetto di qualsiasi callback che non ha semplicemente inviato un token alla coda dei messaggi.
Pemdas,

La risoluzione di un problema di ottimizzazione (come la riduzione al minimo di f (x)) viene spesso implementata fornendo il puntatore a una funzione f (x) alla procedura di ottimizzazione, che "lo richiama" cercando il minimo. Come lo faresti senza una richiamata?
quant_dev

1
Nessun voto negativo, ma i callback non sono malvagi. Chiamare una richiamata mentre si tiene un lucchetto è malvagio. Non chiamare nulla all'interno di un blocco quando non sai se potrebbe bloccare o attendere. Ciò include non solo callback ma anche funzioni virtuali, funzioni API, funzioni in altri moduli ("livello superiore" o "livello inferiore").
Nikie,

@nikie: se un blocco deve essere tenuto durante il callback, il resto dell'API deve essere progettato per rientrare (difficile!) o il fatto che tu sia in possesso di un blocco deve essere una parte documentata dell'API ( sfortunato, ma a volte tutto quello che puoi fare).
Donal Fellows

@Donal Fellows: se si deve tenere un lucchetto durante una richiamata, direi che hai un difetto di progettazione. Se non c'è davvero nessun altro modo, allora sì, sicuramente documentalo! Proprio come documenteresti se il callback verrà chiamato in un thread in background. Fa parte dell'interfaccia.
Nikie,
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.