Quali lezioni hai imparato da un progetto che quasi / effettivamente fallisce a causa di un cattivo multithreading? [chiuso]

11

Quali lezioni hai appreso da un progetto che è quasi / effettivamente fallito a causa di un brutto 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 errore e sento che è meglio per me non lavorare su nulla che abbia a che fare con il multithreading in quel framework.

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

Non riesco a 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 ma sul thread principale (i "thread di lavoro") ), e in cui i dati e i messaggi devono viaggiare in tutte le direzioni tra i componenti N (un grafico completamente connesso).

Nel momento in cui lasciai quel progetto per un altro, c'erano ovunque problemi di stallo. Ho saputo che 2-3 mesi dopo, diversi altri sviluppatori sono riusciti a risolvere tutti i problemi di deadlock, al punto che possono essere spediti ai clienti. Non sono mai riuscito a scoprire quella mancanza di conoscenza che mi manca.

Qualcosa sul progetto: il numero di ID di messaggio (i valori interi che descrivono il significato di un evento che può essere inviato nella coda di messaggi di un altro oggetto, indipendentemente dalla filettatura) vengono eseguiti in diverse migliaia. Anche stringhe univoche (messaggi degli utenti) si trovano a circa un migliaio.

Aggiunto

La migliore analogia che ho avuto da un altro team (estraneo ai miei progetti passati o presenti) era di "mettere i dati in un database". ("Database" che si riferisce alla centralizzazione e agli aggiornamenti atomici). In una GUI frammentata in più viste tutte in esecuzione sullo stesso "thread principale" e tutte le operazioni di sollevamento pesante non della GUI vengono eseguite nei singoli thread di lavoro, i dati dell'applicazione devono essere memorizzati in una singola plase che si comporta come un Database, e lasciare che il "Database" gestisca tutti gli "aggiornamenti atomici" che coinvolgono 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 nella cache le cose e l'utente non se ne accorgerà se è scaduto da una frazione di secondo, se è progettato correttamente. Questo "database" è anche conosciuto come "il documento" nell'architettura Document-View. Purtroppo, no, la mia app memorizza tutti i dati nelle Visualizzazioni. Non so perché è stato così.

Partecipanti:

(i contributori non hanno bisogno di usare esempi reali / personali. Le lezioni da esempi aneddotici, se sono giudicate da te credibili, sono anche benvenute.)

    
posta rwong 09.05.2011 - 05:09
fonte

8 risposte

13

La mia lezione preferita - molto dura! - è che in un programma multithreading lo scheduler è un porcello furtivo che ti odia. Se le cose possono andare storte, lo faranno, ma in modo inaspettato. Ottieni qualcosa di sbagliato, e inseguirai strani heisenbugs (perché qualsiasi strumentazione che aggiungi cambierà i tempi e ti darà un modello di esecuzione diverso).

L'unico modo corretto per risolvere questo problema è rigorosamente correggere tutte le manipolazioni del thread in un piccolo pezzo di codice che va tutto bene e che è molto prudente nell'assicurare che i blocchi siano tenuti correttamente ( e con un ordine di acquisizione globalmente costante). Il modo più semplice per farlo è non condividere la memoria (o altre risorse) tra i thread tranne per i messaggi che devono essere asincroni; che ti permette di scrivere tutto il resto in uno stile che è ignaro del filo. (Bonus: il ridimensionamento su più macchine in un cluster è molto più semplice.)

    
risposta data 09.05.2011 - 10:45
fonte
6

Ecco alcune lezioni di base a cui posso pensare in questo momento (non a causa di progetti falliti ma da problemi reali visti su progetti reali):

  • Cerca di evitare qualsiasi blocco delle chiamate mentre si tiene una risorsa condivisa. Il modello di deadlock comune è il mutex del thread grab, rende un callback, i blocchi di callback sullo stesso mutex.
  • Proteggi l'accesso a tutte le strutture di dati condivise con una sezione mutex / critica (o usa quelle senza blocco - ma non inventare le tue!)
  • Non assumere atomicità: utilizza API atomiche (ad esempio InterlockedIncrement).
  • RTFM riguardante la sicurezza dei thread di librerie, oggetti o API che stai utilizzando.
  • Approfitta delle primitive di sinconizzazione disponibili, ad es. eventi, semafori. (Ma prestate molta attenzione quando li usate sapendo di trovarvi in uno stato buono - Ho visto molti esempi di eventi segnalati in uno stato sbagliato in modo che eventi o dati possano andare persi)
  • Si supponga che le discussioni possano essere eseguite simultaneamente 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 non facciano altre garanzie).
risposta data 09.05.2011 - 07:28
fonte
6
  • L'intero progetto GUI deve essere chiamato solo dal thread principale . Fondamentalmente, non dovresti inserire un singolo ".net" "invoke" nella tua GUI. Il multithreading dovrebbe essere bloccato in progetti separati che gestiscono il più lento accesso ai dati.

Abbiamo ereditato una parte in cui il progetto della GUI utilizza una dozzina di thread. Non sta dando nulla se non problemi. Deadlock, problemi di corsa, chiamate della GUI incrociate ...

    
risposta data 09.05.2011 - 10:14
fonte
1

Java 5 e successivi ha Executor che hanno lo scopo di rendere la vita più facile per la gestione di programmi di stile fork-join multi-threading.

Usale, rimuoverà un sacco di dolore.

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

    
risposta data 09.05.2011 - 08:09
fonte
1

Ho uno sfondo in sistemi embedded hard realtime. Non è possibile verificare l'assenza di problemi causati dal multithreading. (A volte puoi confermare la presenza). Il codice deve essere dimostrabilmente corretto. Quindi best practice su qualsiasi interazione thread.

  • Regola n. 1: BACIO - Se non è necessario un thread, non eseguirne uno. Serializzare come il più possibile.
  • Regola n. 2: non rompere il numero 1.
  • # 3 Se non riesci a dimostrare attraverso la revisione è corretto, non è.
risposta data 09.05.2011 - 09:55
fonte
1

Un'analogia da una lezione sul multithreading che ho preso l'anno scorso è stata molto utile. La sincronizzazione dei thread è come un segnale stradale che protegge un'intersezione (dati) dall'uso da parte di due auto (thread) contemporaneamente. L'errore commesso da molti sviluppatori sta trasformando le luci in rosso in gran parte della città per far passare un'auto perché pensano che sia troppo difficile o pericoloso individuare il segnale esatto di cui hanno bisogno. Potrebbe funzionare bene quando il traffico è leggero, ma porterà all'arresto mentre la tua applicazione cresce.

È una cosa che sapevo già in teoria, ma dopo quella lezione l'analogia mi ha davvero impressionato, e sono rimasto sorpreso di quanto spesso avrei indagato su un problema di threading e trovato una coda gigante, o interruzioni disabilitate ovunque durante una scrittura a una variabile vengono utilizzati solo due thread o i mutex vengono trattenuti per un lungo periodo in cui potrebbe essere refactored per evitarlo del tutto.

In altre parole, alcuni dei peggiori problemi di threading sono causati dall'overkill che tenta di evitare problemi di threading.

    
risposta data 09.05.2011 - 23:55
fonte
0

Prova a rifarlo.

Almeno per me, ciò che ha creato una differenza è stata la pratica. Dopo aver fatto il lavoro multi-thread e distribuito un bel po 'di volte, non ti resta che riprenderlo.

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

Un'altra cosa che sta imparando di più è la struttura dei dati senza blocco.

Penso che questa domanda possa essere davvero migliorata se si specifica il framework. I pool di thread .NET e gli operatori in background sono molto diversi da QThread, ad esempio. Ci sono sempre alcuni trucchi specifici della piattaforma.

    
risposta data 09.05.2011 - 05:44
fonte
0

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

    
risposta data 09.05.2011 - 06:31
fonte

Leggi altre domande sui tag