Threading Best Practices

Molti progetti su cui lavoro hanno implementazioni di threading scadenti e io sono il sucker che deve rintracciarli. C’è un modo migliore accettato per gestire il threading. Il mio codice è sempre in attesa di un evento che non viene mai triggersto.

Sto pensando un po ‘come un disegno o qualcosa del genere.

(Supponendo .NET, cose simili si applicherebbero ad altre piattaforms.)

Bene, ci sono molte cose da considerare. Consiglierei:

  • L’immutabilità è ottima per il multi-threading. La programmazione funzionale funziona bene contemporaneamente in parte grazie all’enfasi sull’immutabilità.
  • Utilizzare i blocchi quando si accede a dati condivisi mutabili, sia per le letture che per le scritture.
  • Non provare ad andare in blocco a meno che non sia davvero necessario. Le serrature sono costose, ma raramente il collo di bottiglia.
  • Monitor.Wait dovrebbe quasi sempre far parte di un ciclo di condizioni, in attesa che una condizione diventi vera e in attesa di nuovo se non lo è.
  • Cerca di evitare di tenere le serrature più a lungo del necessario.
  • Se hai bisogno di acquisire due blocchi contemporaneamente, documenta attentamente l’ordine e assicurati di utilizzare sempre lo stesso ordine.
  • Documenta la sicurezza del thread dei tuoi tipi. La maggior parte dei tipi non ha bisogno di essere thread-safe, devono solo non essere ostili al thread (cioè “puoi usarli da più thread, ma è tua responsabilità estrarre i lock se vuoi condividerli)
  • Non accedere all’interfaccia utente (tranne nei modi documentati thread-safe) da un thread non UI. In Windows Form, utilizzare Control.Invoke / BeginInvoke

Questo è fuori di testa – probabilmente penso di più se questo ti è utile, ma mi fermerò lì nel caso in cui non lo fosse.

Imparare a scrivere correttamente programmi multi-thread è estremamente difficile e richiede molto tempo.

Quindi il primo passo è: sostituire l’implementazione con una che non usa più thread.

Quindi inserisci di nuovo il threading se, e solo se, ne scopri una vera necessità, quando hai trovato dei modi molto semplici e sicuri per farlo. Un’implementazione senza thread che funziona in modo affidabile è molto meglio di un’implementazione con thread non funzionante.

Quando sei pronto per iniziare, preferisci i progetti che utilizzano code thread-safe per trasferire gli elementi di lavoro tra i thread e fare in modo che gli oggetti di lavoro siano accessibili solo da un thread alla volta.

Cerca di evitare di spruzzare blocchi di lock attorno al tuo codice nella speranza che diventerà sicuro per i thread. Non funziona. Alla fine, due percorsi di codice acquisiranno gli stessi blocchi in un ordine diverso e tutto si fermerà (una volta ogni due settimane, sul server di un cliente). Ciò è particolarmente probabile se si combinano i thread con gli eventi di triggerszione e si tiene il blocco mentre si triggers l’evento – il gestore può rimuovere un altro blocco e ora si dispone di un paio di blocchi tenuti in un ordine particolare. E se fossero portati fuori nell’ordine opposto in qualche altra situazione?

In breve, questo è un argomento così grande e difficile che penso sia potenzialmente fuorviante dare alcuni suggerimenti in una risposta breve e dire “Off you go!” – Sono sicuro che non è questa l’intenzione delle molte persone istruite che danno risposte qui, ma questa è l’impressione che molti ottengono dai consigli riassunti.

Invece, compra questo libro .

Ecco un riassunto molto ben fatto di questo sito :

Il multithreading ha anche degli svantaggi. Il più grande è che può portare a programmi molto più complessi. Avere più thread non crea di per sé complessità; è l’interazione tra i fili che crea complessità. Ciò si applica indipendentemente dal fatto che l’interazione sia intenzionale e possa comportare lunghi cicli di sviluppo, oltre a una continua suscettibilità a errori intermittenti e non riproducibili. Per questo motivo, è utile mantenere tale interazione in un design multi-thread semplice o non utilizzare il multithreading a meno che non si abbia una particolare inclinazione per la riscrittura e il debugging!

Riepilogo perfetto da Stroustrup :

Il modo tradizionale di gestire la concorrenza lasciando un mucchio di thread allentati in un unico spazio di indirizzamento e quindi utilizzando le serrature per cercare di affrontare le gare di dati risultanti e i problemi di coordinamento è probabilmente il peggiore ansible in termini di correttezza e comprensibilità.

(Come Jon Skeet, molto di questo presuppone .NET)

A rischio di sembrare polemico, commenti come questi mi infastidiscono:

Imparare a scrivere correttamente programmi multi-thread è estremamente difficile e richiede molto tempo.

I fili dovrebbero essere evitati quando ansible …

È praticamente imansible scrivere software che faccia qualcosa di significativo senza fare leva su thread in qualche modo. Se si è su Windows, aprire il Task Manager, abilitare la colonna Conteggio thread e probabilmente si può contare su un lato il numero di processi che utilizzano un singolo thread. Sì, non si dovrebbero semplicemente usare i fili per il gusto di usare i fili né dovrebbero essere fatti in modo disinvolto, ma francamente, credo che questi clich siano usati troppo spesso.

Se dovessi far bollire la programmazione multithread per il vero novizio, direi questo:

  • Prima di saltarci dentro, devi prima capire che il limite della class non è lo stesso di un limite di filo. Ad esempio, se un metodo di callback sulla class viene chiamato da un altro thread (ad esempio, il delegato AsyncCallback al metodo TcpListener.BeginAcceptTcpClient ()), capire che la callback viene eseguita su quell’altro thread. Pertanto, anche se il callback si verifica sullo stesso object, è comunque necessario sincronizzare l’accesso ai membri dell’object all’interno del metodo di callback. Thread e classi sono ortogonali; è importante capire questo punto.
  • Identificare quali dati devono essere condivisi tra i thread. Una volta definiti i dati condivisi, provare a consolidarli in una singola class, se ansible.
  • Limita i luoghi in cui i dati condivisi possono essere scritti e letti. Se riesci a portare questo in un posto per scrivere e un posto per leggere, ti farai un enorme favore. Questo non è sempre ansible, ma è un bel objective per cui sparare.
  • Ovviamente assicurati di sincronizzare l’accesso ai dati condivisi utilizzando la class Monitor o la parola chiave lock.
  • Se ansible, utilizzare un singolo object per sincronizzare i dati condivisi indipendentemente dal numero di campi condivisi diversi. Questo semplificherà le cose. Tuttavia, può anche vincolare troppo le cose, nel qual caso potrebbe essere necessario un object di sincronizzazione per ogni campo condiviso. E a questo punto, l’uso di classi immutabili diventa molto utile.
  • Se si dispone di un thread che deve segnalare un altro thread, si consiglia vivamente di utilizzare la class ManualResetEvent per eseguire questa operazione anziché utilizzare eventi / delegati.

Per riassumere, direi che il threading non è difficile, ma può essere noioso. Tuttavia, un’applicazione correttamente threaded sarà più retriggers, e i tuoi utenti saranno più riconoscenti.

EDIT: Non c’è nulla di “estremamente difficile” su ThreadPool.QueueUserWorkItem (), delegati asincroni, le varie coppie di metodi BeginXXX / EndXXX, ecc. In C #. Se non altro, queste tecniche rendono molto più semplice eseguire varie attività in modo thread. Se si dispone di un’applicazione GUI che esegue un’interazione pesante di database, socket o I / O, è praticamente imansible rendere il front-end reattivo all’utente senza sfruttare i thread dietro le quinte. Le tecniche che ho citato sopra rendono questo ansible e sono un gioco da ragazzi. È importante capire le insidie, per essere sicuro. Semplicemente credo che i programmatori, specialmente i più giovani, siano un disservizio quando parliamo di come “estremamente difficile” la programmazione multithreaded o di come i thread “dovrebbero essere evitati”. Commenti come questi semplificano eccessivamente il problema ed esagerano il mito quando la verità è che il threading non è mai stato più facile. Esistono motivi legittimi per utilizzare i thread e cliches come questo mi sembrano controproducenti.

Potresti essere interessato a qualcosa come CSP o una delle altre algebre teoriche per gestire la concorrenza. Esistono librerie CSP per la maggior parte delle lingue, ma se il linguaggio non è stato progettato per questo, richiede un po ‘di disciplina per l’uso corretto. Ma alla fine, ogni tipo di concorrenza / threading si riduce ad alcune basi abbastanza semplici: evitare dati mutabili condivisi e capire esattamente quando e perché ogni thread potrebbe dover bloccare mentre attende un altro thread. (In CSP, i dati condivisi semplicemente non esistono. Ogni thread (o processo nella terminologia CSP) è solo permesso di comunicare con gli altri attraverso il blocco dei canali che trasmettono messaggi. Poiché non ci sono dati condivisi, le condizioni di gara vanno via. sta bloccando, diventa facile ragionare sulla sincronizzazione e dimostra letteralmente che non si possono verificare deadlock.)

Un’altra buona pratica, che è più facile da aggiornare in un codice esistente, è assegnare una priorità o un livello a ogni blocco del sistema e assicurarsi che le seguenti regole siano seguite in modo coerente:

  • Mentre si tiene un lucchetto al livello N, si possono acquisire solo nuove serrature di livelli inferiori
  • I blocchi multipli allo stesso livello devono essere acquisiti contemporaneamente, come una singola operazione, che cerca sempre di acquisire tutti i blocchi richiesti nello stesso ordine globale (Si noti che qualsiasi ordine coerente farà, ma qualsiasi thread che cerca di acquisire uno o più blocchi al livello N, devono acquisirli nello stesso ordine in cui qualsiasi altro thread farebbe in qualsiasi altro punto del codice.)

Seguendo queste regole significa che è semplicemente imansible che si verifichi un deadlock. Quindi devi solo preoccuparti dei dati condivisi mutabili.

Grande enfasi sul primo punto che Jon ha pubblicato. Lo stato più immutabile che hai (cioè: globals che sono const, ecc …), più facile sarà la tua vita (cioè: meno blocchi avrai a che fare, meno ragionamenti avrai fare sull’ordine di interleaving, ecc …)

Inoltre, spesso se si dispone di oggetti di piccole dimensioni a cui è necessario avere più thread per avere accesso, a volte è meglio copiarlo tra thread piuttosto che avere un globale condiviso, mutabile che si deve tenere un blocco per leggere / mutare. È un compromesso tra la tua sanità mentale e l’efficienza della memoria.

Cercare un modello di design quando si ha a che fare con i thread è l’approccio migliore per iniziare. È un peccato che molte persone non provino, invece di implementare da soli o meno complessi costrutti multithread.

Sarei probabilmente d’accordo con tutte le opinioni pubblicate finora. Inoltre, consiglierei di utilizzare alcuni framework esistenti a grana più grossa, fornendo blocchi di costruzione piuttosto che semplici strutture come serrature o operazioni di attesa / notifica. Per Java, sarebbe semplicemente il pacchetto java.util.concurrent integrato, che offre classi pronte all’uso che è ansible combinare facilmente per ottenere un’app multithread. Il grande vantaggio di questo è che si evita di scrivere operazioni di basso livello, il che si traduce in codice difficile da leggere e sobject a errori, a favore di una soluzione molto più chiara.

Dalla mia esperienza, sembra che la maggior parte dei problemi di concorrenza possano essere risolti in Java utilizzando questo pacchetto. Ma, ovviamente, devi sempre stare attento con il multithreading, è comunque una sfida.

Aggiungendo ai punti che altre persone hanno già fatto qui:


Alcuni sviluppatori sembrano pensare che il blocco “quasi sufficiente” sia abbastanza buono. È stata la mia esperienza che può essere vero il contrario – il blocco “quasi sufficiente” può essere peggio di un blocco sufficiente.

Immagina il thread Una risorsa di blocco R, usandola e sbloccandola. A utilizza quindi la risorsa R ‘senza blocco.

Nel frattempo, il thread B tenta di accedere a R mentre A lo ha bloccato. Il thread B è bloccato fino a quando il thread A non sblocca R. Quindi il contesto CPU passa al thread B, che accede a R, e quindi aggiorna R ‘durante la sua porzione temporale . Questo aggiornamento rende R ‘incoerente con R, causando un errore quando A tenta di accedervi.


Prova su quante più architetture hardware e OS possibili. Diversi tipi di CPU, diversi numeri di core e chip, Windows / Linux / Unix, ecc.


Il primo sviluppatore che ha lavorato con programmi multi-threaded era un ragazzo di nome Murphy.

Bene, tutti finora sono stati Windows / .NET centric, quindi parlerò con Linux / C.

Evita i futex a tutti i costi (PDF), a meno che tu non abbia davvero bisogno di recuperare parte del tempo trascorso con le serrature mutex. Attualmente sto tirando i capelli con futex Linux.

Non ho ancora il coraggio di andare con le soluzioni prive di serratura , ma mi sto rapidamente avvicinando a quel punto per pura frustrazione. Se riuscissi a trovare un’implementazione valida, ben documentata e portabile di cui sopra, che potrei davvero studiare e comprendere, probabilmente preferirei troncare completamente le discussioni.

Ultimamente mi sono imbattuto in così tanto codice che utilizza thread che in realtà non dovrebbero, è ovvio che qualcuno volesse solo professare il proprio amore per i thread POSIX quando una singola fork (sì, solo una) avrebbe fatto il lavoro.

Vorrei poterti dare un codice che “funziona”, “sempre”. Potrei, ma sarebbe così sciocco da servire come dimostrazione (server e così via che iniziano i thread per ogni connessione). Nelle applicazioni più complesse basate sugli eventi, ho ancora (dopo alcuni anni) scrivere qualcosa che non soffra di misteriosi problemi di concorrenza che sono quasi impossibili da riprodurre. Quindi sono il primo ad ammettere, in quel tipo di applicazione, i fili sono solo un po ‘troppo corda per me. Sono così allettanti e finisco sempre per impiccarmi.

È lo stato mutevole, stupido

Questa è una citazione diretta da Java Concurrency in Practice di Brian Goetz. Anche se il libro è basato su Java, il “Sommario della parte I” fornisce alcuni suggerimenti utili che si applicheranno in molti contesti di programmazione con thread. Ecco alcuni altri dallo stesso riepilogo:

  • Gli oggetti immutabili sono automaticamente thread-safe.
  • Custodisci ogni variabile mutabile con un lucchetto.
  • Un programma che accede a una variabile mutabile da più thread senza sincronizzazione è un programma non funzionante.

Consiglierei di prendere una copia del libro per un trattamento approfondito di questo argomento difficile.

alt text http://www.cs.umd.edu/class/fall2008/cmsc433/jcipMed.jpg

Vorrei seguire il consiglio di Jon Skeet con altri suggerimenti:

  • Se stai scrivendo un “server”, e probabilmente hai una quantità elevata di parallelismo degli inserti, non utilizzare Microsoft Compact SQL. Il suo lock manager è stupido. Se si utilizza SQL Compact, NON utilizzare transazioni serializzabili (che si verifica per impostazione predefinita per la class TransactionScope). Le cose ti crolleranno rapidamente. SQL Compact non supporta le tabelle temporanee e, quando si tenta di simularle all’interno di transazioni serializzate, redicullyuly stupidamente cose come prendere x-blocchi nelle pagine indice della tabella _sysobjects. Inoltre, è davvero entusiasta della promozione del blocco, anche se non si utilizzano le tabelle temporanee. Se è necessario l’accesso seriale a più tabelle, la soluzione migliore è utilizzare transazioni di lettura ripetibili (per fornire atomicità e integrità) e quindi implementare il proprio gestore di blocchi gerarchici basato su oggetti dominio (account, clienti, transazioni, ecc.) Anziché usando lo schema basato su tabella-riga-riga del database.

    Quando si esegue questa operazione, tuttavia, è necessario prestare attenzione (come ha detto John Skeet) per creare una gerarchia di blocco ben definita.

  • Se si crea il proprio gestore di blocco, utilizzare i campi per archiviare le informazioni sui blocchi che si eseguono e quindi aggiungere asserzioni ogni dove all’interno del gestore blocchi che impone le regole di gerarchia di blocco. Ciò contribuirà a sradicare i potenziali problemi in anticipo.

  • In qualsiasi codice eseguito in un thread dell’interfaccia utente, aggiungi gli !InvokeRequired su !InvokeRequired (per winforms) o Dispatcher.CheckAccess() (per WPF). Allo stesso modo si dovrebbe aggiungere l’asserzione inversa al codice che viene eseguito nei thread in background. In questo modo, le persone che osservano un metodo sapranno, solo guardandolo, quali sono i requisiti di threading. Gli asserti aiuteranno anche a catturare insetti.

  • Asserire come un matto, anche nei negozi al dettaglio. (questo significa lanciare, ma puoi far sembrare che i tuoi lanci asseriscano). Un crash dump con un’eccezione che dice “hai violato le regole di threading in questo modo”, insieme alle tracce dello stack, è molto più facile eseguire il debug di un report da un cliente all’altro capo del mondo che dice “ogni tanto l’app mi si congela o sputa un gallo “.

Invece di bloccare i contenitori, dovresti usare ReaderWriterLockSlim. Questo ti dà il blocco del database come un numero infinito di lettori, uno scrittore e la possibilità di aggiornamento.

Per quanto riguarda i modelli di progettazione, pub / sub è abbastanza ben stabilito e molto facile da scrivere in .NET (usando readerwriterlockslim). Nel nostro codice, abbiamo un object MessageDispatcher che tutti ricevono. Ci si iscrive o si invia un messaggio in modo completamente asincrono. Tutto ciò che devi bloccare sono le funzioni registrate e tutte le risorse su cui lavorano. Rende molto più facile il multithreading.