2012-04-01 13 views
24

Esistono molte soluzioni per implementare i thread "user-space". Sia goroutines golang.org, thread verdi di Python, async di C#, processi di erlang, ecc. L'idea è di consentire la programmazione concorrente anche con un numero singolo o limitato di thread.Perché i thread del sistema operativo sono considerati costosi?

Quello che non capisco è, perché i thread del sistema operativo sono così costosi? A mio avviso, in entrambi i casi è necessario salvare lo stack dell'attività (thread del sistema operativo o thread dell'utente), ovvero alcune decine di kilobyte, ed è necessario uno scheduler per spostarsi tra due attività.

Il sistema operativo fornisce entrambe queste funzioni gratuitamente. Perché i thread del sistema operativo dovrebbero essere più costosi dei thread "verdi"? Qual è la ragione del presunto peggioramento delle prestazioni causato dall'avere un thread OS dedicato per ogni "attività"?

+0

Non sono solo considerati costosi, lo sono. Credo che alcuni fili verdi (di Haskell?) Pesino solo un paio di kilobyte ciascuno, cioè cento volte più piccoli. Un altro problema: i thread Python standard non sono verdi - hanno problemi con il multithreading a causa del GIL, ma sono comunque dei veri thread del sistema operativo (forse stai pensando a 'greenlets'?) Quella è una storia diversa, e in effetti simile al verde thread). – delnan

+0

@delnan OK, l'ho sentito. Ma non sono ancora sicuro del perché dovrebbero essere più costosi. Entrambi devono salvare lo stack e fare il context switch (ignorare GIL, ci sono molti esempi non python). –

risposta

11

Desidero modificare la risposta Tudors che è un buon punto di partenza. Esistono due overhead principali di thread:

  1. Avviare e arrestarli. Coinvolge la creazione di uno stack e oggetti del kernel. Coinvolge le transizioni del kernel e i blocchi globali del kernel.
  2. Mantenendo il loro stack in giro.

(1) è solo un problema se si stanno creando e fermandoli tutto il tempo. Questo è risolto comunemente usando i pool di thread. Considero questo problema praticamente risolto. La pianificazione di un'attività in un pool di thread in genere non comporta un intervento sul kernel che lo rende molto veloce. Il sovraccarico è nell'ordine di alcune operazioni di memoria interbloccate e alcune allocazioni.

(2) Questo diventa importante solo se hai molti thread (> 100 o giù di lì). In questo caso, l'IO asincrono è un mezzo per sbarazzarsi dei thread. Ho scoperto che se non si dispone di folle quantità di thread I/O sincrono compreso il blocco è leggermente più veloce di un IO asincrono (avete letto bene: sync IO è più veloce).

+1

(1) Non sono sicuro del motivo per cui gli oggetti del kernel sono più costosi degli oggetti userspace di cui necessitano i blocchi, e tutti i lock si riducono a OS = kernle lock. Non capisco (2) devi comunque mantenere il loro stack. –

+0

Non tutte le alternative di thread mantengono lo stack attorno, ad esempio nel caso in cui un futuro/task non abbia ancora iniziato l'esecuzione. Inoltre, le pile di thread del sistema operativo possono essere più pesanti. Lo stack .NET commette sempre 1MB di memoria (che è sfortunato). – usr

+2

Per quanto riguarda (1): i blocchi non si riducono ai blocchi del kernel. Molte ottimizzazioni sono possibili per serrature incondizionate e/o a breve durata. Gli oggetti del kernel hanno un sovraccarico per molte ragioni (per esempio possono essere condivisi tra processi, possono avere ACL, ...). Richiedono anche una transizione in modalità kernel. – usr

4

Il problema con l'avvio di thread del kernel per ogni piccola attività è che comporta un sovraccarico non trascurabile per l'avvio e l'arresto, in combinazione con le dimensioni dello stack necessarie.

Questo è il primo punto importante: esistono pool di thread in modo da poter riciclare i thread, al fine di evitare perdite di tempo a partire da essi e spreco di memoria per i loro stack.

In secondo luogo, se si spengono i thread per eseguire l'I/O asincrono, passano la maggior parte del tempo bloccati in attesa del completamento dell'I/O, quindi non fanno alcun lavoro e sprecano memoria. Un'opzione molto migliore è quella di avere un singolo operatore a gestire più chiamate asincrone (attraverso alcune tecniche di pianificazione sotto il cofano, come il multiplexing), risparmiando di nuovo memoria e tempo.

Una cosa che rende i thread "verdi" più veloci rispetto ai thread del kernel è che sono oggetti dello spazio utente, gestiti da una macchina virtuale. Iniziarli è una chiamata allo spazio utente, mentre l'avvio di una discussione è una chiamata nello spazio del kernel che è molto più lenta.

+0

Non capisco perché ha un sovraccarico. Com'è diverso con i thread "verdi". Devi mantenere il loro stack, quindi stai sprecando la stessa quantità di memoria. –

+1

@ Chi-Lan: un thread "verde" potrebbe non essere un thread reale, ma un'astrazione di un thread. Diversi thread verdi possono essere programmati in modo intelligente sullo stesso thread del kernel per un uso efficiente, ad esempio utilizzando le fibre di Windows per eseguire la pianificazione cooperativa. – Tudor

+0

@ Chi-Lan: i thread "verdi"/"leggeri" sono creati per evitare questo problema. Esempi di questi sono in Haskell, Erlang e Python. –

0

Penso che le due cose siano in livelli diversi.

Thread o Process è un'istanza del programma in esecuzione. In un processo/thread ci sono molte più cose in esso. Stack di esecuzioni, file di apertura, segnali, stato dei processori e molte altre cose.

Greentlet è diverso, è eseguito in vm. Fornisce un filo leggero.Molti di essi forniscono uno pseudo-concorrente (in genere in un singolo thread o pochi thread di sistema operativo). E spesso forniscono un metodo lock-free con la trasmissione dei dati anziché la condivisione dei dati.

Quindi, le due cose si concentrano in modo diverso, quindi il peso è diverso.

E nella mia mente, il verdetto dovrebbe essere finito nella VM non nel SO.

+1

greenlet è possibile senza vm, vedi golang.org –

6

Esistono molte soluzioni per implementare i thread "user-space". Sia goroutines golang.org, thread verdi di Python, async di C#, processi di erlang, ecc. L'idea è di consentire la programmazione concorrente anche con un numero singolo o limitato di thread.

È un livello di astrazione. È più facile per molte persone cogliere questo concetto e usarlo più efficacemente in molti scenari. È anche più facile per molte macchine (presupponendo una buona astrazione), dal momento che il modello passa dalla larghezza alla trazione in molti casi. Con pthreads (ad esempio), hai tutto il controllo. Con altri modelli di threading, l'idea è di riutilizzare i thread, perché il processo di creazione di un'attività concorrente sia poco costoso e di utilizzare un modello di threading completamente diverso. È molto più facile digerire questo modello; c'è meno da imparare e misurare, ei risultati sono generalmente buoni.

Quello che non capisco è, perché i thread del sistema operativo sono così costosi? A mio avviso, in entrambi i casi è necessario salvare lo stack dell'attività (thread del sistema operativo o thread dell'utente), ovvero alcune decine di kilobyte, ed è necessario uno scheduler per spostarsi tra due attività.

Creare un thread è costoso e lo stack richiede memoria. Inoltre, se il tuo processo utilizza molti thread, il cambio di contesto può uccidere le prestazioni. I modelli di threading così leggeri sono diventati utili per una serie di motivi. La creazione di un thread del sistema operativo è diventata una buona soluzione per le attività di medie e grandi dimensioni, idealmente in numeri bassi. Questo è restrittivo e richiede molto tempo per essere mantenuto.

Un thread di task/thread pool/userland non deve preoccuparsi della commutazione del contesto o della creazione di thread. Spesso è "riutilizzare la risorsa quando diventa disponibile, se non è pronta ora - anche, determinare il numero di thread attivi per questa macchina".

Più comunemente (IMO), i thread di livello SO sono costosi perché non vengono utilizzati correttamente dagli ingegneri: ne esistono troppi e c'è un sacco di cambio di contesto, c'è competizione per lo stesso set di risorse, i compiti sono troppo piccoli Richiede molto più tempo per capire come utilizzare correttamente i thread del sistema operativo e come applicare al meglio il contesto dell'esecuzione di un programma.

Il sistema operativo fornisce entrambe queste funzioni gratuitamente.

Sono disponibili, ma non sono gratuiti. Sono complessi e molto importanti per una buona prestazione. Quando si crea un thread del SO, viene dato il tempo "presto" - tutto il tempo del processo viene diviso tra i thread. Questo non è il caso comune con i thread utente. L'attività viene spesso accodata quando la risorsa non è disponibile. Ciò riduce il cambio di contesto, la memoria e il numero totale di thread che devono essere creati. Quando l'attività termina, il thread ne riceve un altro.

Consideriamo questa analogia di distribuzione temporale:

  • presuppongono che sei in un casinò. Ci sono un numero di persone che vogliono le carte.
  • Hai un numero fisso di rivenditori. Ci sono meno rivenditori che persone che vogliono le carte.
  • Non ci sono sempre abbastanza carte per ogni persona in un dato momento.
  • Le persone hanno bisogno di tutte le carte per completare il proprio gioco/mano. Restituiscono le loro carte al mazziere quando il loro gioco/mano è completo.

Come chiederebbe ai rivenditori di distribuire le carte?

Sotto lo scheduler del sistema operativo, questo sarebbe basato sulla priorità (thread). A ogni persona verrà assegnata una carta alla volta (tempo della CPU) e la priorità verrà valutata continuamente.

Le persone rappresentano l'attività o il lavoro del thread. Le carte rappresentano il tempo e le risorse. I concessionari rappresentano discussioni e risorse.

Come gestiresti più velocemente se ci fossero 2 rivenditori e 3 persone? e se c'erano 5 concessionari e 500 persone? Come puoi minimizzare le carte esaurite da gestire? Con le discussioni, l'aggiunta di carte e l'aggiunta di rivenditori non è una soluzione che puoi fornire "su richiesta". L'aggiunta di CPU equivale all'aggiunta di rivenditori. L'aggiunta di thread è equivalente ai dealer che distribuiscono le carte a più persone alla volta (aumenta il cambio di contesto). Esistono diverse strategie per distribuire le carte più rapidamente, soprattutto dopo aver eliminato il bisogno di carte delle persone in un determinato periodo di tempo. Non sarebbe più veloce andare a un tavolo e occuparsi di una persona o di una persona fino a quando il gioco non sarà completo se il rapporto tra il dealer e le persone è di 1/50? Confrontalo con la visita di ogni tabella in base alla priorità e coordinando le visite tra tutti i rivenditori (l'approccio del sistema operativo). Ciò non vuol dire che il sistema operativo sia stupido - ciò implica che la creazione di un thread del sistema operativo è un ingegnere che aggiunge più persone e più tabelle, potenzialmente più di quanto i rivenditori possano ragionevolmente gestire. Fortunatamente, i vincoli possono essere risolti in molti casi utilizzando altri modelli di multithreading e astrazioni più elevate.

Perché i thread del sistema operativo dovrebbero essere più costosi dei fili "verdi"? Qual è la ragione del presunto peggioramento delle prestazioni causato dall'avere un thread OS dedicato per ogni "attività"?

Se avete sviluppato una libreria di threading prestazioni critiche di basso livello (ad esempio su di pthreads), si dovrebbe riconoscere l'importanza del riutilizzo (e implementarlo nella libreria come un modello disponibile per gli utenti). Da questo punto di vista, l'importanza dei modelli di multithreading di livello superiore è una soluzione/ottimizzazione semplice ed evidente basata sull'utilizzo del mondo reale e l'ideale che la barra di inserimento per l'adozione e l'utilizzo efficace del multithreading può essere ridotta.

Non è che siano costosi: il modello e il pool dei thread leggeri rappresentano la soluzione migliore per molti problemi e un'astrazione più appropriata per gli ingegneri che non capiscono bene i thread. La complessità del multithreading è notevolmente semplificata (e spesso più performante nell'uso del mondo reale) con questo modello. Con i thread del sistema operativo, si ha un maggiore controllo, ma occorre fare parecchie altre considerazioni per utilizzarli nel modo più efficace possibile - tenere conto di queste considerazioni può ripercuotere drammaticamente l'esecuzione/l'implementazione di un programma. Con astrazioni di livello più elevato, molte di queste complessità sono minimizzate modificando completamente il flusso dell'esecuzione dell'attività (larghezza contro pull).

6

Il salvataggio dello stack è banale, a prescindere dalle sue dimensioni: il puntatore dello stack deve essere salvato nel blocco informazioni del thread nel kernel, (così solitamente si risparmia anche la maggior parte dei registri dato che saranno stati spinti da qualunque l'interruzione soft/hard ha causato l'inserimento del sistema operativo).

Un problema è che è richiesto un ciclo di protezione a livello dell'anello per immettere il kernel dall'utente. Questo è un overhead essenziale, ma fastidioso.Quindi il driver o la chiamata di sistema deve eseguire tutto ciò che è stato richiesto dall'interrupt e quindi la pianificazione/distribuzione dei thread sui processori. Se ciò comporta la prelazione di un thread da un processo a un thread da un altro, deve essere scambiato anche un carico di contesto di processo aggiuntivo. Viene aggiunto un ulteriore overhead se il sistema operativo decide che un thread che è in esecuzione su un altro core del processore rispetto a quello che gestisce il mut di interrupt deve essere preimpostato - l'altro core deve essere interrotto dall'hardware, (questo è in cima all'interruzione hard/soft che entred il sistema operativo in primo luogo.

Quindi, una corsa programmazione può essere un'operazione piuttosto complessa.

'fili verdi' o 'fibre' sono, (di solito), prevista dal codice utente. a al contesto il cambiamento è molto più semplice ed economico di un interrupt del sistema operativo ecc. perché non è richiesto alcun ciclo di loop Wagneriano su ogni cambiamento di contesto, il contesto del processo non cambia e il thread del sistema operativo che esegue il gruppo di thread verde non cambia

Poiché qualcosa per nulla non esiste, ci sono problemi con i fili verdi. Sono gestiti da thread "reali" del sistema operativo. Ciò significa che se un thread "verde" in un gruppo eseguito da un thread del sistema operativo fa bloccare una chiamata del sistema operativo, tutti i thread verdi nel gruppo vengono bloccati. Ciò significa che le chiamate semplici come sleep() devono essere "emulate" da una macchina a stati che fornisce ad altri thread verdi, (sì, proprio come re-implementare il sistema operativo). Allo stesso modo, qualsiasi segnalazione inter-thread.

Inoltre, naturalmente, i fili verdi non possono rispondere direttamente al segnale di I/O, quindi in qualche modo sconfiggere il punto di avere qualsiasi thread in primo luogo.

1

A person in Google shows an interesting approach.

Secondo lui, modalità kernel commutazione non è il collo di bottiglia, e il costo nucleo accada in SMP scheduler. E sostiene che la pianificazione di M: N assistita dal kernel non sarebbe costosa, e questo mi fa aspettare che il threading M: N generale sia disponibile in tutte le lingue.

Problemi correlati