Perché creare un thread è costoso?


180

I tutorial Java affermano che la creazione di un thread è costosa. Ma perché è esattamente costoso? Cosa sta succedendo esattamente quando viene creato un thread Java che ne rende costosa la creazione? Sto prendendo la dichiarazione come vera, ma mi interessa solo la meccanica della creazione di Thread in JVM.

Sovraccarico del ciclo di vita del thread. La creazione e lo smontaggio del thread non sono gratuiti. L'overhead effettivo varia tra le piattaforme, ma la creazione del thread richiede tempo, introducendo latenza nell'elaborazione della richiesta e richiede alcune attività di elaborazione da parte della JVM e del sistema operativo. Se le richieste sono frequenti e leggere, come nella maggior parte delle applicazioni server, la creazione di un nuovo thread per ogni richiesta può consumare risorse di elaborazione significative.

Dalla concorrenza Java in pratica
di Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Stampa ISBN-10: 0-321-34960-1


Non conosco il contesto in cui i tutorial che hai letto dicono questo: implicano che la creazione stessa è costosa o che "creare un thread" è costoso. La differenza che provo a mostrare è tra la pura azione di creare il thread (chiamiamolo un'istanza o qualcosa del genere), o il fatto che tu abbia un thread (quindi usando un thread: ovviamente avendo un overhead). Quale è richiesto // quale desideri chiedere?
Nanne,

9
@typoknig - Costoso rispetto al NON creare un nuovo thread :)
willcodejavaforfood


1
threadpools per la vittoria. non è necessario creare sempre nuovi thread per le attività.
Alexander Mills,

Risposte:


149

La creazione di thread Java è costosa perché è coinvolto un bel po 'di lavoro:

  • Un grande blocco di memoria deve essere allocato e inizializzato per lo stack di thread.
  • È necessario effettuare chiamate di sistema per creare / registrare il thread nativo con il sistema operativo host.
  • I descrittori devono essere creati, inizializzati e aggiunti alle strutture dati interne alla JVM.

È anche costoso nel senso che il filo limita le risorse finché è vivo; ad esempio lo stack di thread, tutti gli oggetti raggiungibili dallo stack, i descrittori di thread JVM, i descrittori di thread nativi del sistema operativo.

I costi di tutte queste cose sono specifici della piattaforma, ma non sono economici su nessuna piattaforma Java che abbia mai incontrato.


Una ricerca su Google mi ha trovato un vecchio punto di riferimento che riporta una velocità di creazione di thread di ~ 4000 al secondo su un Sun Java 1.4.1 su un Xeon dual vintage del 2002 con Linux vintage 2002. Una piattaforma più moderna fornirà numeri migliori ... e non posso commentare la metodologia ... ma almeno fornisce un campo di battaglia per quanto sia probabile la costosa creazione di thread.

Il benchmarking di Peter Lawrey indica che la creazione di thread è significativamente più veloce in questi giorni in termini assoluti, ma non è chiaro quanto di ciò siano dovuti miglioramenti in Java e / o nel sistema operativo ... o velocità del processore più elevate. Ma i suoi numeri indicano ancora un miglioramento di oltre 150 volte se si utilizza un pool di thread rispetto alla creazione / avvio di un nuovo thread ogni volta. (E sottolinea che questo è tutto relativo ...)


(Quanto sopra presuppone "thread nativi" anziché "thread verdi", ma i JVM moderni utilizzano tutti thread nativi per motivi di prestazioni. I thread verdi sono probabilmente più economici da creare, ma si paga in altre aree.)


Ho fatto un po 'di ricerche per vedere come viene realmente allocato lo stack di un thread Java. Nel caso di OpenJDK 6 su Linux, lo stack di thread viene allocato dalla chiamata a pthread_createquello che crea il thread nativo. (La JVM non passa pthread_createuno stack preallocato.)

Quindi, all'interno pthread_createdello stack viene assegnato da una chiamata mmapcome segue:

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

Secondo man mmap, il MAP_ANONYMOUSflag fa inizializzare la memoria a zero.

Pertanto, anche se potrebbe non essere essenziale che i nuovi stack di thread Java siano azzerati (secondo le specifiche JVM), in pratica (almeno con OpenJDK 6 su Linux) sono azzerati.


2
@Raedwald - è la parte di inizializzazione che è costosa. Da qualche parte, qualcosa (ad esempio il GC o il sistema operativo) azzererà i byte prima che il blocco venga trasformato in uno stack di thread. Ciò richiede cicli di memoria fisica su hardware tipico.
Stephen C

2
"Da qualche parte, qualcosa (ad es. GC o OS) azzererà i byte". Lo farà? Il sistema operativo lo farà se richiede l'allocazione di una nuova pagina di memoria, per motivi di sicurezza. Ma sarà raro. E il sistema operativo potrebbe conservare una cache di pagine già a zero (IIRC, Linux lo fa). Perché il GC dovrebbe preoccuparsi, dato che la JVM impedirà a qualsiasi programma Java di leggere il suo contenuto? Si noti che la malloc()funzione C standard , che potrebbe essere utilizzata dalla JVM, non garantisce che la memoria allocata sia a zero (presumibilmente per evitare tali problemi di prestazioni).
Raedwald,

1
stackoverflow.com/questions/2117072/… concorda sul fatto che "Un fattore importante è la memoria dello stack allocata per ogni thread".
Raedwald,

2
@Raedwald: vedere la risposta aggiornata per informazioni su come viene effettivamente allocato lo stack.
Stephen C,

2
È possibile (anche probabile) che le pagine di memoria allocate dalla mmap()chiamata siano mappate copia su scrittura su una pagina zero, quindi la loro initailizzazione non avviene all'interno di mmap()se stessa, ma quando le pagine vengono prima scritte su, e quindi solo una pagina in un tempo. Cioè, quando il thread inizia l'esecuzione, con il costo sostenuto dal thread creato anziché dal thread del creatore.
Raedwald,

76

Altri hanno discusso da dove provengono i costi del threading. Questa risposta spiega perché la creazione di un thread non è così costosa rispetto a molte operazioni, ma relativamente costosa rispetto alle alternative di esecuzione delle attività, che sono relativamente meno costose.

L'alternativa più ovvia all'esecuzione di un'attività in un altro thread è eseguire l'attività nello stesso thread. Questo è difficile da capire per coloro che presumono che più thread siano sempre migliori. La logica è che se l'overhead dell'aggiunta dell'attività a un altro thread è maggiore del tempo risparmiato, può essere più veloce eseguire l'attività nel thread corrente.

Un'altra alternativa è utilizzare un pool di thread. Un pool di thread può essere più efficiente per due motivi. 1) riutilizza i thread già creati. 2) è possibile ottimizzare / controllare il numero di thread per garantire prestazioni ottimali.

Il seguente programma stampa ....

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

Questo è un test per un'attività banale che espone il sovraccarico di ogni opzione di threading. (Questa attività di test è il tipo di attività che viene effettivamente eseguita meglio nel thread corrente.)

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

Come puoi vedere, la creazione di un nuovo thread costa solo ~ 70 µs. Questo potrebbe essere considerato banale in molti casi d'uso, se non nella maggior parte. Relativamente parlando è più costoso delle alternative e per alcune situazioni un pool di thread o non usare affatto i thread è una soluzione migliore.


8
Questo è un ottimo pezzo di codice lì. Conciso, al punto e mostra chiaramente il suo jist.
Nicholas

Nell'ultimo blocco, credo che il risultato sia distorto, perché nei primi due blocchi il thread principale viene rimosso in parallelo mentre i thread di lavoro stanno inserendo. Tuttavia, nell'ultimo blocco, l'azione di prendere viene eseguita in serie, quindi sta dilatando il valore. Probabilmente potresti usare queue.clear () e usare CountDownLatch invece di attendere il completamento dei thread.
Victor Grazi,

@VictorGrazi Suppongo che tu voglia raccogliere i risultati a livello centrale. Sta facendo lo stesso lavoro di accodamento in ogni caso. Un blocco del conto alla rovescia sarebbe leggermente più veloce.
Peter Lawrey,

In realtà, perché non farlo fare in modo coerente e veloce, come aumentare un contatore? rilascia l'intera cosa BlockingQueue. Controlla il contatore alla fine per impedire al compilatore di ottimizzare l'operazione di incremento
Victor Grazi

@grazi potresti farlo in questo caso, ma non nella maggior parte dei casi realistici in quanto l'attesa su un contatore potrebbe essere inefficiente. Se lo facessi, la differenza tra gli esempi sarebbe ancora maggiore.
Peter Lawrey,

31

In teoria, questo dipende dalla JVM. In pratica, ogni thread ha una quantità relativamente grande di memoria dello stack (256 KB per impostazione predefinita, credo). Inoltre, i thread sono implementati come thread del sistema operativo, quindi la loro creazione comporta una chiamata al sistema operativo, ovvero un cambio di contesto.

Renditi conto che "costoso" nell'informatica è sempre molto relativo. La creazione di thread è molto costosa rispetto alla creazione della maggior parte degli oggetti, ma non molto costosa rispetto a una ricerca casuale del disco rigido. Non devi evitare di creare discussioni a tutti i costi, ma crearne centinaia al secondo non è una mossa intelligente. Nella maggior parte dei casi, se il progetto richiede molti thread, è necessario utilizzare un pool di thread di dimensioni limitate.


9
Tra kb = kilo-bit, kB = kilo byte. Gb = bit giga, GB = byte giga.
Peter Lawrey,

@PeterLawrey capitalizziamo 'k' in 'kb' e 'kB', quindi c'è una simmetria tra 'Gb' e 'GB'? Queste cose mi infastidiscono.
Jack,

3
@Jack C'è un K= 1024 e k= 1000.;) en.wikipedia.org/wiki/Kibibyte
Peter Lawrey,

9

Esistono due tipi di thread:

  1. Thread appropriati : si tratta di astrazioni attorno alle funzionalità di threading del sistema operativo sottostante. La creazione di thread è quindi costosa quanto quella del sistema: c'è sempre un sovraccarico.

  2. Discussioni "verdi" : create e programmate dalla JVM, sono più economiche, ma non si verifica un paralellismo adeguato. Questi si comportano come thread, ma vengono eseguiti all'interno del thread JVM nel sistema operativo. Non sono spesso usati, per quanto ne sappia.

Il fattore più grande che mi viene in mente nel sovraccarico di creazione del thread è la dimensione dello stack che hai definito per i tuoi thread. Le dimensioni dello stack del thread possono essere passate come parametro durante l'esecuzione della VM.

Oltre a ciò, la creazione di thread dipende principalmente dal sistema operativo e persino dall'implementazione della VM.

Ora lasciatemi sottolineare qualcosa: la creazione di thread è costosa se si prevede di sparare 2000 thread al secondo, ogni secondo del tempo di esecuzione. JVM non è progettato per gestirlo . Se avrai un paio di lavoratori stabili che non verranno licenziati e uccisi più e più volte, rilassati.


19
"... un paio di lavoratori stabili che non verranno licenziati e uccisi ..." Perché ho iniziato a pensare alle condizioni del posto di lavoro? :-)
Stephen C

6

La creazione Threadsrichiede l'allocazione di una discreta quantità di memoria poiché non deve crearne una, ma due nuove pile (una per il codice Java, una per il codice nativo). L'uso di Executors / Thread Pools può evitare il sovraccarico, riutilizzando i thread per più attività per Executor .


@Raedwald, qual è la jvm che utilizza stack separati?
bestsss

1
Philip JP dice 2 pile.
Raedwald,

Per quanto ne so, tutte le JVM assegnano due stack per thread. È utile per la garbage collection trattare il codice Java (anche quando JITed) in modo diverso dal free-casting c.
Philip JF,

@Philip JF Puoi per favore elaborare? Cosa intendi per 2 pile una per codice Java e una per codice nativo? Che cosa fa?
Gurinder,

"Per quanto ne so, tutte le JVM assegnano due stack per thread." - Non ho mai visto alcuna prova a sostegno di questo. Forse stai fraintendendo la vera natura dell'opstack nelle specifiche JVM. (È un modo di modellare il comportamento dei bytecode, non qualcosa che deve essere usato in fase di esecuzione per eseguirli.)
Stephen C

1

Ovviamente il nocciolo della domanda è cosa significa "costoso".

Un thread deve creare uno stack e inizializzare lo stack in base al metodo run.

Deve impostare strutture di stato di controllo, ovvero quale stato è eseguibile, in attesa ecc.

Probabilmente c'è una buona quantità di sincronizzazione nell'impostare queste cose.

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.