Come spiegare perché il multi-threading è difficile


84

Sono un programmatore abbastanza bravo, il mio capo è anche un programmatore abbastanza bravo. Anche se sembra sottovalutare alcuni compiti come il multi-threading e quanto possa essere difficile (lo trovo molto difficile per qualcosa di più che eseguire alcuni thread, aspettare che finiscano tutti, quindi restituire risultati).

Nel momento in cui inizi a doverti preoccupare di deadlock e condizioni di gara, lo trovo molto difficile, ma il boss non sembra apprezzarlo - non penso che l'abbia mai incontrato. Basta schiacciare un lucchetto su di esso è praticamente l'atteggiamento.

Quindi, come posso presentarlo o spiegare perché potrebbe sottovalutare le complessità di concorrenza, parallelismo e multi-threading? O forse mi sbaglio?

Modifica: solo un po 'di quello che ha fatto - scorrere un elenco, per ogni elemento in quell'elenco creare un thread che esegue un comando di aggiornamento del database in base alle informazioni in quell'elemento. Non sono sicuro di come abbia controllato quanti thread eseguiti contemporaneamente, suppongo che avrebbe dovuto aggiungerli a una coda se ce ne fossero stati troppi in esecuzione (non avrebbe usato un semaforo).


17
Il multi-threading è semplice. La sincronizzazione corretta è difficile.
Vineet Reynolds,

33
Porta tre persone nella stanza, preferibilmente con accenti diversi, e fai spiegare loro parti diverse e sovrapposte del problema della concorrenza ... contemporaneamente.
Greyfade,

Il multithreading può essere molto difficile o molto semplice, a seconda del problema attuale e del supporto linguistico. Clojure semplifica clojure.org/concurrent_programming
Giobbe

4
@Job La programmazione concorrente è sempre difficile (nei progetti del mondo reale), indipendentemente dal linguaggio che stai utilizzando. Scala, Clojure o Erlang lo rendono un po 'sano quando si desidera confrontarlo con le lingue che usano e incoraggiano stati mutevoli.
Chirone,

4
La mia metafora preferita per questo è: "Prenderesti un sonnifero e un lassativo allo stesso tempo?" Anche usando code di messaggi complesse, l'ordine è il frutto della concorrenza fatto nel modo giusto . Ciò, a meno che tu non abbia una grande esperienza con esso, è difficile per molte persone.
Tim Post

Risposte:


29
  1. Se puoi contare su qualsiasi esperienza matematica, illustra come un normale flusso di esecuzione essenzialmente deterministico diventi non solo non deterministico con diversi thread, ma esponenzialmente complesso, perché devi assicurarti che ogni possibile interfogliatura delle istruzioni della macchina farà ancora la cosa giusta. Un semplice esempio di aggiornamento perso o situazione di lettura sporca è spesso una rivelazione.

  2. "Slap a lock on it" è la soluzione banale ... risolve tutti i tuoi problemi se non ti preoccupi delle prestazioni. Prova a illustrare quanto sarebbe un grande successo se, ad esempio, Amazon dovesse bloccare l'intera costa orientale ogni volta che qualcuno ad Atlanta ordina un libro!


1
+1 per la discussione della complessità matematica: è così che ho capito la difficoltà nella concorrenza tra stati condivisi, ed è l'argomento che generalmente propongo nel sostenere architetture che passano messaggi. -1 per "schiaffeggia un lucchetto su di esso" ... La frase connota un approccio sconsiderato all'uso dei blocchi, che molto probabilmente porterà a deadlock o a comportamenti incoerenti (poiché i client del tuo codice che vivono in thread diversi creano conflitti richieste, ma non si sincronizzano tra loro, i client avranno modelli incompatibili dello stato della tua libreria).
Aidan Cully,

2
Amazon non ha per bloccare l'inventario di un singolo elemento a un magazzino brevemente durante l'elaborazione di ordine. Se si verifica una corsa improvvisa ed enorme su un particolare articolo, le prestazioni degli ordini per quell'articolo subiranno fino a quando la fornitura non sarà esaurita e l'accesso all'inventario diventerà di sola lettura (e quindi condivisibile al 100%). Una cosa che Amazon sostiene che altri programmi non lo fanno è la possibilità di mettere in coda gli ordini fino a quando non si verifica un nuovo stock e l'opzione di gestire gli ordini in coda prima che un nuovo stock sia reso disponibile per i nuovi ordini.
Blrfl,

@Blrfl: i programmi possono farlo se sono scritti per utilizzare il passaggio di messaggi tramite le code. Non è necessario che tutti i messaggi a un determinato thread passino attraverso una singola coda ...
Donal Fellows

4
@Donal Fellows: se ci sono 1 milione di widget in stock in un magazzino e 1 milione di ordini arrivano nello stesso istante, tutte queste richieste vengono serializzate a un certo livello mentre si abbinano gli articoli agli ordini, indipendentemente da come vengono gestiti. La realtà pratica è che Amazon probabilmente non ha mai così tanti widget disponibili che la latenza nell'elaborazione di una cotta di ordini diventa inaccettabilmente alta prima che l'inventario si esaurisca e tutti gli altri in fila possano essere informati (in parallelo) "siamo fuori. " Le code dei messaggi sono un ottimo modo per prevenire i deadlock, ma non risolvono il problema della contesa elevata per una risorsa limitata.
Blrfl,

79

Il multi-threading è semplice. Codificare un'applicazione per il multi-threading è molto, molto semplice.

C'è un semplice trucco, e questo è usare una coda di messaggi ben progettata ( non rotolare la tua) per passare i dati tra i thread.

La parte difficile sta cercando di fare in modo che più thread aggiornino magicamente un oggetto condiviso in qualche modo. Questo è quando diventa soggetto a errori perché la gente non presta attenzione alle condizioni di gara presenti.

Molte persone non usano le code dei messaggi e provano ad aggiornare oggetti condivisi e creare problemi per se stessi.

Ciò che diventa difficile è la progettazione di un algoritmo che funzioni bene quando si passano i dati tra più code. È difficile. Ma la meccanica dei thread coesistenti (tramite code condivise) è semplice.

Inoltre, si noti che i thread condividono le risorse I / O. È improbabile che un programma associato I / O (ad es. Connessioni di rete, operazioni sui file o operazioni sul database) vada più veloce con molti thread.

Se vuoi illustrare il problema di aggiornamento dell'oggetto condiviso, è semplice. Siediti sul tavolo con un mazzo di carte di carta. Scrivi una semplice serie di calcoli - 4 o 6 semplici formule - con un sacco di spazio in fondo alla pagina.

Ecco il gioco. Ognuno di voi legge una formula, scrive una risposta e mette una carta con la risposta.

Ognuno di voi farà metà del lavoro, giusto? Hai finito in metà tempo, giusto?

Se il tuo capo non pensa molto e inizia appena, finirai per essere in conflitto in qualche modo ed entrambi scrivere risposte alla stessa formula. Non ha funzionato perché c'è una condizione di razza intrinseca tra voi due prima di scrivere. Nulla ti impedisce di leggere la stessa formula e di sovrascrivere le risposte reciproche.

Esistono molti, molti modi per creare condizioni di gara con risorse mal bloccate o non bloccate.

Se si desidera evitare tutti i conflitti, tagliare la carta in una pila di formule. Prendi uno dalla coda, scrivi la risposta e pubblica le risposte. Nessun conflitto perché entrambi leggete da una coda di messaggi a un solo lettore.


Anche tagliare la carta in una pila non risolve completamente le cose: hai ancora la situazione in cui tu e il tuo capo raggiungete una nuova formula allo stesso modo e colpite le nocche nella sua. In effetti, direi che questo è rappresentativo del tipo più comune di problema di threading. Gli errori davvero grossolani si trovano presto. Gli errori davvero insoliti rimangono per sempre perché nessuno può riprodurli, le condizioni di gara plausibili - come questa - continuano a spuntare nei test e alla fine tutti (o più probabilmente la maggior parte) vengono risolti.
Airsource Ltd,

@AirsourceLtd Che cosa stai dicendo esattamente "colpisci le tue nocche con le sue"? Finché hai una coda di messaggi che impedisce a due thread diversi di accettare lo stesso messaggio, non sarebbe un problema. A meno che non fraintenda quello che intendevi.
Zack,

25

La programmazione multi-thread è probabilmente la soluzione più difficile alla concorrenza. Fondamentalmente è un'astrazione di livello piuttosto basso di ciò che la macchina effettivamente fa.

Esistono diversi approcci, come il modello dell'attore o la memoria transazionale (software) , che sono molto più facili. O lavorando con strutture di dati immutabili (come elenchi e alberi).

In generale, una corretta separazione delle preoccupazioni rende più semplice il multi-threading. Qualcosa, spesso dimenticato, quando le persone generano 20 thread, tutti tentando di elaborare lo stesso buffer. Utilizzare i reattori in cui è necessaria la sincronizzazione e in genere passare i dati tra diversi lavoratori con code di messaggi.
Se hai un blocco nella logica dell'applicazione, hai fatto qualcosa di sbagliato.

Quindi sì, tecnicamente, il multi-threading è difficile.
"Slap a lock on it" è praticamente la soluzione meno scalabile ai problemi di concorrenza e in realtà vanifica l'intero scopo del multi-threading. Ciò che fa è ripristinare un problema in un modello di esecuzione non simultaneo. Più lo fai, più è probabile che tu abbia un solo thread in esecuzione alla volta (o 0 in un deadlock). Sconfigge l'intero scopo.
È come dire "Risolvere i problemi del terzo mondo è facile. Basta lanciarci una bomba". Solo perché esiste una soluzione banale, questo non rende il problema banale, poiché ti preoccupi della qualità del risultato.

Ma in pratica, risolvere questi problemi è tanto difficile quanto qualsiasi altro problema di programmazione ed è meglio farlo con astrazioni appropriate. Il che lo rende abbastanza facile in effetti.


14

Penso che ci sia un angolo non tecnico di questa domanda: l'IMO è una questione di fiducia. Di solito ci viene chiesto di riprodurre app complesse come - oh, non lo so - Facebook per esempio. Sono giunto alla conclusione che se devi spiegare la complessità di un compito ai non iniziati / alla direzione, allora qualcosa di marcio in Danimarca.

Anche se altri programmatori ninja potessero svolgere il compito in 5 minuti, le tue stime si basano sulle tue capacità personali. Il tuo interlocutore dovrebbe imparare a fidarsi della tua opinione in merito o assumere qualcuno la cui parola è disposta ad accettare.

La sfida non è nel trasmettere le implicazioni tecniche, che le persone tendono a ignorare o non sono in grado di comprendere attraverso la conversazione, ma a stabilire una relazione di reciproco rispetto.


1
Risposta interessante, sebbene si tratti di una domanda tecnica. Sono comunque d'accordo con quello che dici ... in questo caso, tuttavia, il mio manager è un programmatore piuttosto bravo, tuttavia penso solo che non ha riscontrato la complessità delle app multi-thread, le sottovaluta.
Mr Shoubs,

6

Un semplice esperimento mentale per comprendere i deadlock è il problema del " cenare filosofo ". Uno degli esempi che tendo a usare per descrivere le cattive condizioni di gara può essere la situazione Therac 25 .

"Basta schiacciare un lucchetto su di esso" è la mentalità di qualcuno che non ha incontrato bug difficili con il multi-threading. Ed è possibile che egli pensa che si sta esagerando la gravità della situazione (non lo faccio - è possibile far saltare in aria roba o uccidere le persone con condizioni di gara insetti, in particolare con il software integrato che finisce in auto).


1
cioè il problema del sandwich: fai un mucchio di sandwich, ma c'è solo 1 piatto di burro e 1 coltello. In generale va tutto bene, ma alla fine qualcuno afferrerà il burro mentre qualcun altro afferrerà il coltello .. e poi rimarranno entrambi in attesa che l'altro lasci andare le loro risorse.
gbjbaanb,

È possibile risolvere problemi di questo tipo acquistando sempre risorse in un ordine prestabilito?
compman,

@compman, no. Perché è possibile che 2 thread provino ad afferrare la stessa risorsa nello stesso momento e quei thread non necessitano necessariamente dello stesso insieme di risorse - solo una sovrapposizione sufficiente per causare problemi. Uno schema è di "ripristinare" la risorsa e quindi attendere un periodo casuale prima di afferrarla nuovamente. Questo periodo di backoff avviene in una serie di protocolli, il primo dei quali è stato Aloha. en.wikipedia.org/wiki/ALOHAnet
Tangurena,

1
E se ogni risorsa nel programma avesse un numero e quando un thread / processo necessita di un insieme di risorse, blocca sempre le risorse in ordine numerico crescente? Non penso che potrebbe verificarsi un deadlock.
compman,

1
@compman: questo è davvero un modo per evitare un deadlock. È possibile progettare strumenti che ti consentano di verificarlo automaticamente; quindi se la tua applicazione non trova mai blocchi di risorse se non in ordine numerico crescente, non hai mai avuto un potenziale deadlock. (Si noti che i potenziali deadlock si trasformano in veri e propri deadlock solo quando il codice viene eseguito sul computer di un cliente).
gnasher729,

3

Le applicazioni simultanee non sono deterministiche. Con la quantità eccezionalmente piccola di codice complessivo che il programmatore ha riconosciuto come vulnerabile, non si controlla quando una parte di un thread / processo viene eseguita in relazione a qualsiasi parte di un altro thread. Il test è più difficile, richiede più tempo ed è improbabile che trovi tutti i difetti relativi alla concorrenza. I difetti, se riscontrati, sono spesso sottili e non possono essere riprodotti in modo coerente, quindi il fissaggio è difficile.

Pertanto l'unica applicazione concorrente corretta è quella che è dimostrabilmente corretta, qualcosa che spesso non viene praticato nello sviluppo del software. Di conseguenza, la risposta di S.Lot è il miglior consiglio generale, poiché la trasmissione dei messaggi è relativamente facile da dimostrare corretta.


3

Risposta breve in due parole: NON DETERMINISMO OSSERVABILE

Risposta lunga: dipende dall'approccio alla programmazione simultanea che usi in considerazione del tuo problema. Nel libro Concepts, Techniques e Models of Computer Programming , gli autori spiegano chiaramente quattro principali approcci pratici alla scrittura di programmi concorrenti:

  • Programmazione sequenziale : un approccio di base che non ha concorrenza;
  • Concorrenza dichiarativa : utilizzabile in assenza di non determinismo osservabile;
  • Concorrenza di passaggio di messaggi: messaggio simultaneo che passa tra molte entità, in cui ciascuna entità elabora il messaggio internamente in sequenza;
  • Concorrenza dello stato condiviso : thread che aggiorna gli oggetti passivi condivisi mediante azioni atomiche a grana grossa, ad esempio blocchi, monitor e transazioni;

Ora il più semplice di questi quattro approcci oltre all'ovvia programmazione sequenziale è la concorrenza dichiarativa , perché i programmi scritti usando questo approccio non hanno alcun determinismo osservabile . In altre parole, non ci sono condizioni di razza , poiché le condizioni di razza sono solo un comportamento non deterministico osservabile.

Ma la mancanza di non determinismo osservabile significa che ci sono alcuni problemi che non possiamo affrontare usando la concorrenza dichiarativa. Qui è dove entrano in gioco gli ultimi due approcci non così facili. La parte non così facile è una conseguenza del non determinismo osservabile. Ora rientrano entrambi nel modello concomitante con stato e sono anche equivalenti nell'espressività. Ma a causa del numero sempre crescente di core per CPU, sembra che il settore abbia recentemente preso più interesse nella concorrenza sul passaggio dei messaggi, come si può vedere dall'aumento delle librerie di messaggi (ad es. Akka per JVM) o dai linguaggi di programmazione (ad es. Erlang ) .

La libreria Akka menzionata in precedenza, supportata da un modello di attore teorico semplifica la creazione di applicazioni simultanee, poiché non è più necessario gestire blocchi, monitor o transazioni. D'altra parte, richiede un approccio diverso alla progettazione della soluzione, vale a dire pensare in modo gerarchicamente composito agli attori. Si potrebbe dire che richiede una mentalità totalmente diversa, che alla fine può essere ancora più difficile rispetto all'utilizzo della concorrenza condivisa allo stato normale.

La programmazione concorrente è difficile a causa del non determinismo osservabile, ma quando si utilizza l'approccio giusto per il problema dato e la libreria giusta che supporta tale approccio, è possibile evitare molti problemi.


0

Mi è stato inizialmente insegnato che potrebbe far emergere problemi vedendo un semplice programma che ha avviato 2 thread e li ha entrambi stampati sulla console contemporaneamente da 1-100. Invece di:

1
1
2
2
3
3
...

Ottieni qualcosa di più simile a questo:

1
2
1
3
2
3
...

Eseguilo di nuovo e potresti ottenere risultati totalmente diversi.

Molti di noi sono stati addestrati ad assumere che il nostro codice verrà eseguito in sequenza. Con la maggior parte del multi-threading non possiamo dare per scontato "out of the box".


-3

Prova a usare diversi martelli per schiacciare un mucchio di chiodi ravvicinati contemporaneamente senza alcuna comunicazione tra quelli che tengono i martelli ... (supponi che siano bendati).

Inoltra questo alla costruzione di una casa.

Ora puoi dormire la notte imagnando che sei l'architetto. :)


-3

Parte facile: usa il multithreading con funzionalità contemporanee di framework, sistemi operativi e hardware, come semafori, code, contatori interbloccati, tipi di scatole atomiche ecc.

Parte difficile: implementare le funzionalità stesse non utilizzando funzionalità al primo posto, possono essere solo poche funzionalità hardware molto limitate, basandosi solo su garanzie di coerenza di clock su più core.


3
La parte difficile è davvero più difficile, ma anche quella parte facile non è così facile.
PeterAllenWebb,
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.