Macchine a stati vs thread


24

Alan Cox ha detto una volta "Un computer è una macchina a stati. I thread sono per le persone che non possono programmare macchine a stati".
Dato che chiedere direttamente ad Alan non è un'opzione per umiliarmi, preferirei chiedere qui: come si può ottenere la funzionalità multi-thread in un linguaggio di alto livello, come Java, usando solo un thread e una macchina a stati? Ad esempio, cosa succede se ci sono 2 attività da svolgere (fare calcoli e fare I / O) e un'attività può bloccare?
L'uso della modalità "solo macchina a stati" è una valida alternativa al multi-threading in linguaggi di alto livello?


4
Un computer è anche una macchina di Turing. Tuttavia, non è necessariamente utile programmarlo come una macchina di Turing. Lo stack in linguaggi imperativi è estremamente utile e i programmi multithread consentono di mantenere più stack in memoria contemporaneamente. Fare lo stesso in una macchina a stati è sicuramente possibile, ma tanto più disordinato.
thiton,

10
Alan era uno sviluppatore del kernel del sistema operativo; questo era il suo domiano . Quindi la sua citazione dovrebbe essere presa in quel contesto. Avrebbe programmato "contro il metallo" dove ha più senso usare un tale modello. Una volta che il sistema operativo estrae l'hardware e le sue caratteristiche intrinseche (che "un computer è una macchina a stati ...") hai l'opportunità e il vantaggio di poter usare altri modelli che hanno più senso nel tuo dominio . Quasi ogni gioco fa un uso pesante di macchine statali.
Steven Evers,

2
Il threading è semplicemente una funzionalità del sistema operativo per gestire automaticamente alcuni degli switch della macchina a stati, se lo desideri. Ovviamente puoi creare un'enorme macchina statale che gestirà tutto da sola, ma è più complicato. Lo stesso si può dire dei processi. Si può dire che i processi siano destinati anche a persone che non sono in grado di programmare macchine a stati. Ma l'astrazione ti fornisce un'interfaccia molto più semplice e meno soggetta a errori. Secondo me questa è solo un'altra "bella citazione" che dovrebbe essere ascoltata, contemplata e quindi ignorata nella realtà.
Yam Marcovic,

2
"Ma l'astrazione [thread] ti fornisce un'interfaccia molto più semplice e meno soggetta a errori." Sembra falso. Il numero di persone che hanno sbagliato la sicurezza dei thread indica che sembra causare errori.
S.Lott

1
Molti dei commenti e delle risposte qui interpretano la citazione come anti-multitasking in generale; Credo che Alan Cox sia semplicemente un anti-thread e raccomanderebbe l'uso di più processi per raggiungere molti degli obiettivi per cui le persone usano i thread. Ricorda che è un hacker Unix: fork FTW. Non ho trovato alcun commento da lui direttamente sulla citazione, ma eccone uno di Larry McVoy della mailing list del kernel Linux che va in questa direzione: lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
Martin B,

Risposte:


25

Tutto ciò che un thread fa sono operazioni interleave in modo che parti del processo sembrino sovrapporsi nel tempo. Una macchina single-core con più thread si limita a saltare: esegue piccoli bit di codice da un thread, quindi passa a un altro thread. Un semplice scheduler decide quale thread ha la massima priorità e viene effettivamente eseguito nel core.

Su un computer single-core, in realtà non accade nulla "contemporaneamente". È solo un'esecuzione interlacciata.

Esistono molti, molti modi per ottenere l'interleaving. Molti.

Supponiamo che tu abbia un semplice processo a due thread che utilizza un semplice blocco in modo che entrambi i thread possano scrivere su una variabile comune. Hai sei blocchi di codice.

  • T1-prima del blocco
  • T1-con serratura
  • T1-dopo blocco
  • T2 prima del blocco
  • T2-con serratura
  • T2-dopo blocco

[Questo può essere in un ciclo o avere più blocchi o altro. Tutto ciò che fa è allungarsi, non più complesso.]

Le fasi di T1 devono essere eseguite in ordine (T1-prima, T1-con, T1-dopo) e le fasi di T2 devono essere eseguite in ordine (T2-prima, T2-con, T2-dopo).

Oltre al vincolo "in ordine", questi possono essere interfogliati in qualsiasi modo. Comunque. Potrebbero essere eseguiti come elencato sopra. Un altro ordine valido è (T1-prima, T2-prima, T2-blocco, T1-blocco, T2-dopo, T1-dopo). Ci sono molti ordini validi.

Aspettare.

Questa è solo una macchina a stati con sei stati.

Si tratta di automi a stati finiti non deterministici. L'ordinamento degli stati T1-xxx con stati T2-xxx è indeterminato e non ha importanza. Quindi ci sono posti in cui il "prossimo stato" è un lancio di monete.

Ad esempio, all'avvio di FSM, T1-before o T2-before sono entrambi i primi stati legittimi. Lancia una moneta.

Diciamo che è arrivato T1 prima. Fai quello. Al termine, è possibile scegliere tra T1-with e T2-before. Lancia una moneta.

Ad ogni passo nel FSM ci saranno due scelte (due thread - due scelte) e un lancio di una moneta può determinare quale stato specifico è seguito.


Grazie, bella spiegazione. E la macchina multi-core? Immagino, non esiste un modo esplicito per sfruttare i core nella macchina a stati Java? Bisogna fare affidamento sul sistema operativo per questo, giusto?
Victor Sorokin,

5
Una macchina multi-core rende la programmazione leggermente più complessa. Tuttavia, tutti i core scrivono su una singola memoria comune, quindi l'ordinamento di scrittura della memoria tra i due core significa che, essenzialmente, siamo tornati all'esecuzione interfogliata delle scritture di memoria. Il sistema operativo sfrutta i core e JVM lo sfrutta. Non c'è bisogno di pensarci due volte.
S.Lott

9

Scrivere funzioni di blocco è per le persone che non possono creare macchine a stati;)

I thread sono utili se non riesci a aggirare il blocco. Nessuna attività informatica fondamentale sta veramente bloccando, è solo che molti di loro sono implementati in questo modo per facilità d'uso. Invece di restituire un carattere o "lettura non riuscita", una funzione di lettura si blocca fino a quando non viene letto l'intero buffer. Invece di controllare il messaggio di ritorno in una coda e di restituirlo se non viene trovato nessuno, una funzione di connessione attende la risposta.

Non è possibile utilizzare le funzioni di blocco in una macchina a stati (almeno una a cui non è possibile "congelare").

E sì, l'uso della macchina a stati è una valida alternativa. Nei sistemi Real Time, questa è l'unica opzione, il sistema fornisce un framework per la macchina. L'uso dei thread e delle funzioni di blocco è solo "la via più semplice", perché di solito una chiamata a una funzione di blocco sostituisce circa 3-4 stati nella macchina a stati.


Un errore nella tabella codici in un programma a contesto singolo di esecuzione è fondamentalmente veramente bloccante. Per definizione, il codice che ha un unico contesto di esecuzione non può avanzare fino a quando non sarà disponibile la successiva porzione di codice nel flusso.
David Schwartz,

1
@David Schwartz: vero, sta fondamentalmente bloccando; ma non è un '"operazione" poiché non è qualcosa che fa il codice bloccato, è qualcosa che accade ad esso.
Javier,

1
La lettura del file non sta sostanzialmente bloccando: può sempre essere suddivisa in richiesta di lettura di una determinata posizione e acquisizione dei dati dai buffer dopo che la richiesta è stata soddisfatta. E un errore di pagina è una soluzione alternativa per l'utilizzo di scambio casuale / euristico. Si verificherà se il dato stato è stato inserito prima che fossero resi disponibili tutti i dati necessari per la sua esecuzione - una mancanza di lungimiranza, qualcosa contro il concetto di macchina a stati. Se le operazioni di scambio e scambio fanno parte della macchina a stati, allora non si verificherà un errore di pagina.
SF.

1
@David Schwartz: la definizione del comportamento di "blocco" è sempre soggetta a requisiti "in tempo reale". Ad esempio: l'errore della tabella codici è considerato non bloccante per l'applicazione che richiede reattività nell'ordine di centinaia di millisecondi. D'altra parte, se l'applicazione ha rigorosi requisiti in tempo reale, non utilizzerà affatto la memoria virtuale.
MaR

1
@Mar: ... oppure utilizza un algoritmo di scambio deterministico che garantisca il recupero dei dati richiesti prima che diventino necessari.
SF.

9

Come si ottiene la funzionalità multi-threading in un linguaggio di alto livello, come Java, usando solo un thread e una macchina a stati? Ad esempio, cosa succede se ci sono 2 attività da svolgere (fare calcoli e fare I / O) e un'attività può bloccare?

Quello che stai descrivendo si chiama multitasking cooperativo , in cui le attività vengono assegnate alla CPU e si prevede che lo abbandonino volontariamente dopo un certo periodo di tempo o attività autodeterminato. Un'attività che non coopera continuando a utilizzare la CPU o bloccando le gengive su tutto il lavoro e senza un timer di watchdog hardware, non c'è nulla che il codice che supervisiona le attività possa fare al riguardo.

Quello che vedi nei sistemi moderni si chiama multitasking preventivo , che è dove le attività non devono abbandonare la CPU perché il supervisore lo fa per loro quando arriva un interruzione generata dall'hardware. La routine di servizio di interrupt nel supervisore salva lo stato della CPU e lo ripristina la volta successiva che l'attività viene considerata meritevole di un intervallo di tempo, quindi ripristina lo stato da qualsiasi attività debba essere eseguita in seguito e salta indietro come se nulla fosse accaduto . Questa azione si chiama switch di contesto e può essere costosa.

L'uso della modalità "solo macchina a stati" è una valida alternativa al multi-threading in linguaggi di alto livello?

Valida? Sicuro. Sane? A volte. Sia che tu usi i thread o una qualche forma di multitasking cooperativo fatto in casa (ad es. Macchine statali) dipende dai compromessi che sei disposto a fare.

I thread semplificano la progettazione delle attività al punto in cui è possibile trattare ognuno come il proprio programma che condivide lo spazio dati con gli altri. Questo ti dà la libertà di concentrarti sul lavoro a portata di mano e non tutta la gestione e le pulizie necessarie per farlo funzionare un'iterazione alla volta. Ma dal momento che nessuna buona azione rimane impunita, si paga per tutta questa comodità nei cambi di contesto. Avere molti thread che producono la CPU dopo aver svolto un lavoro minimo (volontariamente o facendo qualcosa che si bloccherebbe, come l'I / O) può far perdere molto tempo al processore facendo il cambio di contesto. Ciò è particolarmente vero se le tue operazioni di blocco raramente si bloccano per molto tempo.

Ci sono alcune situazioni in cui il percorso cooperativo ha più senso. Una volta ho dovuto scrivere del software userland per un componente hardware che trasmetteva molti canali di dati attraverso un'interfaccia mappata in memoria che richiedeva il polling. Ogni canale era un oggetto costruito in modo tale che potessi lasciarlo girare come thread o eseguire ripetutamente un singolo ciclo di polling.

Le prestazioni della versione multithread non sono state affatto buone per esattamente il motivo che ho descritto sopra: ogni thread stava facendo un lavoro minimo e quindi cedendo la CPU in modo che gli altri canali potessero avere un po 'di tempo, causando molti cambi di contesto. Consentire ai thread di funzionare liberamente fino a quando non sono stati aiutati dalla velocità effettiva, ma ha comportato la mancata manutenzione di alcuni canali prima che l'hardware subisse un sovraccarico del buffer perché non ottenevano un intervallo di tempo abbastanza presto.

La versione a thread singolo, che eseguiva persino iterazioni di ciascun canale, correva come una scimmia ustionata e il carico sul sistema calava come una roccia. La penalità che ho pagato per la prestazione aggiuntiva è stata quella di destreggiarsi tra i compiti. In questo caso, il codice per farlo era abbastanza semplice che il costo di svilupparlo e mantenerlo valeva il miglioramento delle prestazioni. Immagino che sia davvero la linea di fondo. Se i miei thread fossero stati quelli che restavano in attesa di una chiamata di sistema per tornare, l'esercizio probabilmente non sarebbe valsa la pena.

Questo mi porta al commento di Cox: i thread non sono esclusivamente per le persone che non sono in grado di scrivere macchine a stati. Alcune persone sono abbastanza capaci di farlo, ma scelgono di usare una macchina a stati fissi (cioè un thread) nell'interesse di portare a termine il lavoro prima o con meno complessità.


2

cosa succede se ci sono 2 attività da svolgere (fare calcoli e fare I / O) e un'attività può bloccare?

Beh, onestamente non riesco a immaginare come gestire l'I / O di blocco senza thread. Dopotutto si chiama blocco solo perché il codice che lo invoca deve wait.

Secondo la mia lettura dell'email originale di Cox (sotto), sottolinea che il threading non si adatta bene. Voglio dire, cosa succede se ci sono 100 richieste I / O? 1000? 10000? Cox sta sottolineando che avere un numero elevato di thread può portare a gravi problemi:

Da: Alan Cox (alan@lxorguk.ukuu.org.uk)
Data: ven 21 gen 2000 - 13:33:52 EST

l'articolo IBM), che se la tua applicazione dipende da un numero enorme di thread, continuerai sempre a scontrarti con lo scheduler? un sacco di gente lancia un sacco di discussioni su un problema e può davvero essere un cattivo design.

Questa è l'ultima delle tue preoccupazioni. 1000 thread sono 8Mb di stack del kernel e un cambio di attività sufficiente per essere sicuri di poter disattivare la maggior parte della cache. Un computer è una macchina a stati. I thread sono per le persone che non possono programmare macchine a stati.

Ci sono molti casi in cui Linux sicuramente non aiuta la situazione, in particolare l'I / O a blocchi asincroni.

Alan

fonte: Ri: Interessante analisi del threading del kernel Linux da parte di IBM (archivi della mailing list del kernel Linux)


1
  • In teoria, questo è vero. Nella vita reale, i thread sono solo un'astrazione efficiente utilizzata per programmare una tale macchina a stati. Sono così efficienti che possono essere utilizzati anche per programmare Statecharts e reti di Petri (ovvero comportamenti paralleli, in cui le macchine a stati sono sostanzialmente sequenziali).

  • Il problema con le macchine a stati è l'esplosione combinatoria. Il numero di stati di un computer con 4G RAM è 2 ^ (2 ^ 32) stati (senza contare l'unità disco 2T).

  • A un uomo il cui unico strumento è un martello, ogni problema sembra un chiodo.


1

Le discussioni sono l'unica opzione in due casi:

  • utilizzare più core senza separazione della memoria.
  • per far fronte al codice esterno che blocca.

Il secondo è il motivo per cui la maggior parte delle persone pensa che i thread siano inevitabili per eseguire la programmazione IO o di rete, ma questo di solito è perché non sanno che il loro sistema operativo ha un'API più avanzata (o non vogliono combattere con il loro utilizzo).

Per quanto riguarda la facilità d'uso e la leggibilità, ci sono sempre loop di eventi (come libev o EventMachine ) che rendono la programmazione di una macchina a stati quasi semplice come farlo con i thread, ma dando abbastanza controllo da dimenticare i problemi di sincronizzazione.


1
Il meglio di entrambi i mondi: le piccole macchine a stati bloccanti nei thread rendono molto semplice il codice dell'applicazione. E i fili si dividono bene in nuclei se li hai. Se tutto non ha almeno due core, lo farà presto. ovvero telefoni quad-core basati sul braccio in arrivo nel 2012.
Tim Williscroft,

0

Un buon modo per capire come interagiscono le macchine a stati e il multithreading è guardare i gestori di eventi della GUI. Molte applicazioni / framework GUI utilizzano un singolo thread GUI che eseguirà il polling delle possibili fonti di input e chiamerà una funzione per ciascun input ricevuto; in sostanza, questo potrebbe essere scritto come un enorme interruttore:

while (true) {
    switch (event) {
        case ButtonPressed:
        ...
        case MachineIsBurning:
        ....
    }
}

Ora, diventa abbastanza chiaro che il livello di controllo di alto livello in questo costrutto non può essere alto: il gestore di ButtonPressed deve terminare senza interazione dell'utente e tornare al ciclo principale, perché in caso contrario, nessun ulteriore evento dell'utente può essere elaborato. Se ha uno stato da salvare, questo stato deve essere salvato in variabili globali o statiche, ma non nello stack; vale a dire, il normale flusso di controllo in un linguaggio imperativo è limitato. Sei essenzialmente limitato a una macchina a stati.

Questo può diventare piuttosto disordinato quando si hanno subroutine nidificate che devono salvare, ad esempio, un livello di ricorsione. Oppure stai leggendo un file, ma il file non è al momento disponibile. O sono solo in un lungo calcolo. In tutti questi casi, diventa desiderabile salvare lo stato dell'esecuzione corrente e tornare al ciclo principale, e questo è multithreading . Niente di più, niente di meno.

Il tutto è diventato un po 'più complicato con l'introduzione del multithreading preventivo (ovvero il sistema operativo che decide quando i thread dovrebbero cedere il controllo), ed è per questo che la connessione non è immediatamente chiara oggi.

Quindi, per rispondere alla domanda finale: Sì, la macchina a stati è un'alternativa, la maggior parte delle GUI funziona in questo modo con il thread GUI. Basta non spingere troppo la macchina statale, diventa irraggiungibile molto rapidamente.


0

Chiedere se l'uso di una macchina a stati sia praticabile in un linguaggio di alto livello è un po 'come chiedere se scrivere in assembler sia una valida alternativa all'uso di un linguaggio di alto livello. Entrambi hanno il loro posto, vista la giusta situazione.

L'astrazione dell'utilizzo del threading rende i sistemi paralleli più complessi più facili da implementare, ma alla fine tutti i sistemi paralleli hanno gli stessi problemi da affrontare. Problemi classici come Deadlock / Livelock e inversione di priorità sono altrettanto possibili con i sistemi basati su macchine a stati come lo sono con un parallelo di memoria condivisa , NUMA o persino basato su CSP , se è abbastanza complesso.


0

Non credo che sia - certo, le macchine a stati sono un concetto di elaborazione molto "elegante" ma, come dici tu, sono piuttosto complicate. E le cose complicate sono difficili da ottenere. E le cose che non sono giuste sono semplicemente rotte, quindi a meno che tu non sia un genio della presunta statura di Alan Cox, mantieni le cose che conosci funzionano - lascia la "programmazione intelligente" ai progetti di apprendimento.

Puoi dire quando qualcuno ha tentato invano di farlo, poiché (supponendo che funzioni bene) quando si tratta di mantenerlo, scopri che il compito è quasi impossibile. Il "genio" originale si è mosso lasciandoti con il grumo di codice appena comprensibile (poiché questo tipo di sviluppatori non tende a lasciare troppi commenti e tanto meno documentazione tecnica).

In alcuni casi, una macchina a stati sarà una scelta migliore: sto pensando a elementi di tipo embedded ora in cui vengono utilizzati alcuni modelli di macchine a stati e utilizzati ripetutamente e in modo più formalizzato (ovvero ingegneria adeguata :))

Anche il threading può essere difficile da ottenere correttamente, ma ci sono modelli che ti aiutano lungo, principalmente riducendo la necessità di condividere i dati tra i thread.

L'ultimo punto a riguardo è che i computer moderni funzionano comunque su molti core, quindi una macchina a stati non trarrà davvero un buon vantaggio dalle risorse disponibili. Il threading può fare un lavoro migliore qui.


2
Le macchine a stati non sono affatto complicate! Le macchine a stati complessi sono complicate, ma lo sono anche tutti i sistemi complessi: o)
MaR

2
-1 per "non provare". questo è il peggior consiglio che puoi dare.
Javier,

1
-1 "Non provare"? È solo sciocco. Vorrei anche contestare la tua affermazione che le macchine a stati sono difficili. Una volta che entri in qualcosa come una Heirarchal Fuzzy State Machine ... allora sì, è un po 'più complicato. Ma una semplice macchina a stati? Sono cose piuttosto semplici che ogni 2 anni ho imparato quando ero a scuola.
Steven Evers,

fammi riformulare il po 'non provare' ...
gbjbaanb,

0

Un buon esempio di utilizzo corretto della macchina a stati invece dei thread: nginx vs apache2. Generalmente puoi supporre che nginx gestisca tutte le connessioni in un thread, apache2 crea un thread per connessione.

Ma per me usare macchine a stati vs thread è abbastanza simile usando asm vs java perfettamente fatti a mano: puoi ottenere risultati illeggibili, ma ci vogliono un sacco di sforzi da parte dei programmatori, molta disciplina, rendono il progetto più complesso e vale solo se usato da molti altri programmatori. Quindi, se sei tu quello che vuole creare un web server veloce - usa i macchinette di stato e asincrona l'Io. Se stai scrivendo il progetto (non la libreria da utilizzare ovunque), usa i thread.

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.