Warum soll das Erstellen eines Threads teuer sein?

179

In den Java-Tutorials heißt es, dass das Erstellen eines Threads teuer ist. Aber warum genau ist es teuer? Was genau passiert, wenn ein Java-Thread erstellt wird, dessen Erstellung teuer wird? Ich nehme die Aussage als wahr an, aber ich interessiere mich nur für die Mechanik der Thread-Erstellung in JVM.

Thread-Lebenszyklus-Overhead. Das Erstellen und Herunterfahren von Threads ist nicht kostenlos. Der tatsächliche Overhead variiert je nach Plattform, aber die Thread-Erstellung nimmt Zeit in Anspruch, führt zu einer Latenz bei der Anforderungsverarbeitung und erfordert einige Verarbeitungsaktivitäten von JVM und Betriebssystem. Wenn Anforderungen häufig und einfach sind, wie in den meisten Serveranwendungen, kann das Erstellen eines neuen Threads für jede Anforderung erhebliche Rechenressourcen beanspruchen.

Aus der Java-Parallelität in der Praxis
Von Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes und Doug Lea
Print ISBN-10: 0-321-34960-1

Kachanov
quelle
Ich weiß nicht, in welchem ​​Kontext die von Ihnen gelesenen Tutorials Folgendes sagen: Bedeuten sie, dass die Erstellung selbst teuer ist oder dass das Erstellen eines Threads teuer ist? Der Unterschied, den ich zu zeigen versuche, besteht zwischen der reinen Aktion, den Thread zu erstellen (nennen wir ihn Instanziieren oder so), oder der Tatsache, dass Sie einen Thread haben (also einen Thread verwenden: offensichtlich Overhead). Welches wird beansprucht // nach welchem ​​möchten Sie fragen?
Nanne
9
@typoknig - Teuer im Vergleich zum NICHT Erstellen eines neuen Threads :)
willcodejavaforfood
Mögliches Duplikat des Java-Thread-Erstellungsaufwands
Paul Draper
1
Threadpools für den Gewinn. Sie müssen nicht immer neue Threads für Aufgaben erstellen.
Alexander Mills

Antworten:

148

Die Erstellung von Java-Threads ist teuer, da einiges an Arbeit erforderlich ist:

  • Für den Thread-Stack muss ein großer Speicherblock zugewiesen und initialisiert werden.
  • Es müssen Systemaufrufe durchgeführt werden, um den nativen Thread beim Host-Betriebssystem zu erstellen / zu registrieren.
  • Deskriptoren müssen erstellt, initialisiert und zu JVM-internen Datenstrukturen hinzugefügt werden.

Es ist auch in dem Sinne teuer, dass der Thread Ressourcen bindet, solange er lebt. zB der Thread-Stapel, alle vom Stapel aus erreichbaren Objekte, die JVM-Thread-Deskriptoren, die systemeigenen Thread-Deskriptoren des Betriebssystems.

Die Kosten für all diese Dinge sind plattformspezifisch, aber auf keiner Java-Plattform, auf die ich jemals gestoßen bin, sind sie billig.


Eine Google-Suche ergab für mich einen alten Benchmark , der eine Thread-Erstellungsrate von ~ 4000 pro Sekunde auf einem Sun Java 1.4.1 auf einem 2002er Dual-Prozessor Xeon mit 2002er Vintage Linux angibt. Eine modernere Plattform liefert bessere Zahlen ... und ich kann die Methodik nicht kommentieren ... aber sie gibt zumindest einen Überblick darüber, wie teuer die Erstellung von Threads wahrscheinlich ist.

Das Benchmarking von Peter Lawrey zeigt, dass die Thread-Erstellung heutzutage absolut gesehen erheblich schneller ist, aber es ist unklar, wie viel davon auf Verbesserungen in Java und / oder dem Betriebssystem zurückzuführen ist ... oder auf höhere Prozessorgeschwindigkeiten. Seine Zahlen deuten jedoch immer noch auf eine mehr als 150-fache Verbesserung hin, wenn Sie einen Thread-Pool verwenden und nicht jedes Mal einen neuen Thread erstellen / starten. (Und er macht den Punkt, dass dies alles relativ ist ...)


(Das oben Gesagte setzt "native Threads" anstelle von "grünen Threads" voraus, aber moderne JVMs verwenden aus Leistungsgründen alle native Threads. Grüne Threads sind möglicherweise billiger zu erstellen, aber Sie zahlen in anderen Bereichen dafür.)


Ich habe ein bisschen gegraben, um zu sehen, wie der Stapel eines Java-Threads wirklich zugewiesen wird. Im Fall von OpenJDK 6 unter Linux wird der Thread-Stack durch den Aufruf zugewiesen pthread_create, der den nativen Thread erstellt. (Die JVM pthread_createübergibt keinen vorab zugewiesenen Stapel.)

Dann wird innerhalb pthread_createdes Stapels durch einen Aufruf mmapFolgendes zugewiesen :

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

Entsprechend bewirkt man mmapdas MAP_ANONYMOUSFlag, dass der Speicher auf Null initialisiert wird.

Obwohl es möglicherweise nicht unbedingt erforderlich ist, dass neue Java-Thread-Stapel (gemäß der JVM-Spezifikation) auf Null gesetzt werden, werden sie in der Praxis (zumindest mit OpenJDK 6 unter Linux) auf Null gesetzt.

Stephen C.
quelle
2
@ Raedwald - es ist der Initialisierungsteil, der teuer ist. Irgendwo wird etwas (z. B. der GC oder das Betriebssystem) die Bytes auf Null setzen, bevor der Block in einen Thread-Stapel umgewandelt wird. Dies erfordert physische Speicherzyklen auf typischer Hardware.
Stephen C
2
"Irgendwo wird etwas (z. B. der GC oder das Betriebssystem) die Bytes auf Null setzen". Es wird? Das Betriebssystem wird aus Sicherheitsgründen eine neue Speicherseite zuweisen müssen. Aber das wird ungewöhnlich sein. Und das Betriebssystem behält möglicherweise einen Cache mit bereits nulled ed Seiten (IIRC, Linux tut dies). Warum sollte sich der GC die Mühe machen, da die JVM verhindert, dass ein Java-Programm seinen Inhalt liest? Beachten Sie, dass die Standard-C- malloc()Funktion, die die JVM möglicherweise verwendet, nicht garantiert, dass der zugewiesene Speicher auf Null gesetzt ist (vermutlich, um genau solche Leistungsprobleme zu vermeiden).
Raedwald
1
stackoverflow.com/questions/2117072/… stimmt zu, dass "ein Hauptfaktor der jedem Thread zugewiesene Stapelspeicher ist".
Raedwald
2
@Raedwald - Informationen zur tatsächlichen Zuordnung des Stapels finden Sie in der aktualisierten Antwort.
Stephen C
2
Es ist möglich (wahrscheinlich auch) , dass die Speicher durch den zugeordneten Seiten mmap()Aufruf sind copy-on-write auf eine Seite Null zugeordnet, so dass ihre initailisation nicht innerhalb geschieht mmap()selbst, sondern wenn die Seiten werden zuerst geschrieben , und dann nur eine Seite an eine Zeit. Das heißt, wenn der Thread mit der Ausführung beginnt, werden die Kosten vom erstellten Thread und nicht vom Ersteller-Thread getragen.
Raedwald
76

Andere haben diskutiert, woher die Kosten für das Einfädeln kommen. Diese Antwort behandelt, warum das Erstellen eines Threads im Vergleich zu vielen Operationen nicht so teuer ist, aber im Vergleich zu Alternativen zur Aufgabenausführung, die relativ kostengünstig sind, relativ teuer ist.

Die naheliegendste Alternative zum Ausführen einer Aufgabe in einem anderen Thread besteht darin, die Aufgabe im selben Thread auszuführen. Dies ist schwer zu verstehen für diejenigen, die davon ausgehen, dass mehr Threads immer besser sind. Die Logik ist, dass die Ausführung der Aufgabe im aktuellen Thread schneller sein kann, wenn der Aufwand für das Hinzufügen der Aufgabe zu einem anderen Thread größer ist als die Zeit, die Sie sparen.

Eine andere Alternative ist die Verwendung eines Thread-Pools. Ein Thread-Pool kann aus zwei Gründen effizienter sein. 1) Es verwendet bereits erstellte Threads. 2) Sie können die Anzahl der Threads einstellen / steuern, um sicherzustellen, dass Sie eine optimale Leistung erzielen.

Das folgende Programm druckt ....

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

Dies ist ein Test für eine einfache Aufgabe, bei der der Overhead jeder Threading-Option offengelegt wird. (Diese Testaufgabe ist die Art von Aufgabe, die im aktuellen Thread am besten ausgeführt wird.)

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);
    }
}
}

Wie Sie sehen können, kostet das Erstellen eines neuen Threads nur ~ 70 µs. Dies kann in vielen, wenn nicht den meisten Anwendungsfällen als trivial angesehen werden. Relativ gesehen ist es teurer als die Alternativen und in einigen Situationen ist ein Thread-Pool oder die Nichtverwendung von Threads eine bessere Lösung.

Peter Lawrey
quelle
8
Das ist dort ein großartiger Code. Prägnant, auf den Punkt und zeigt deutlich seinen Jist.
Nicholas
Im letzten Block glaube ich, dass das Ergebnis verzerrt ist, weil in den ersten beiden Blöcken der Haupt-Thread parallel entfernt wird, während die Worker-Threads setzen. Im letzten Block wird die Aktion der Einnahme jedoch alle seriell ausgeführt, sodass der Wert erweitert wird. Sie könnten wahrscheinlich queue.clear () verwenden und stattdessen einen CountDownLatch verwenden, um auf den Abschluss der Threads zu warten.
Victor Grazi
@ VictorGrazi Ich gehe davon aus, dass Sie die Ergebnisse zentral erfassen möchten. Es erledigt jeweils die gleiche Menge an Warteschlangenarbeit. Ein Countdown-Latch wäre etwas schneller.
Peter Lawrey
Warum sollte es nicht einfach etwas konstant schnelles tun, wie das Erhöhen eines Zählers? Lass die ganze BlockingQueue-Sache fallen. Überprüfen Sie den Zähler am Ende, um zu verhindern, dass der Compiler die Inkrementierungsoperation optimiert
Victor Grazi,
@grazi Sie könnten das in diesem Fall tun, aber Sie würden es in den meisten realistischen Fällen nicht tun, da das Warten auf einen Schalter ineffizient sein könnte. Wenn Sie das tun würden, wäre der Unterschied zwischen den Beispielen noch größer.
Peter Lawrey
31

Theoretisch hängt dies von der JVM ab. In der Praxis verfügt jeder Thread über eine relativ große Menge an Stapelspeicher (256 KB pro Standard, glaube ich). Darüber hinaus werden Threads als Betriebssystem-Threads implementiert. Das Erstellen dieser Threads erfordert daher einen Betriebssystemaufruf, dh einen Kontextwechsel.

Beachten Sie, dass "teuer" beim Rechnen immer sehr relativ ist. Die Thread-Erstellung ist im Vergleich zur Erstellung der meisten Objekte sehr teuer, aber im Vergleich zu einer zufälligen Festplattensuche nicht sehr teuer. Sie müssen das Erstellen von Threads nicht um jeden Preis vermeiden, aber das Erstellen von Hunderten von Threads pro Sekunde ist kein kluger Schachzug. In den meisten Fällen sollten Sie einen Thread-Pool mit begrenzter Größe verwenden, wenn Ihr Design viele Threads erfordert.

Michael Borgwardt
quelle
9
Übrigens kb = Kilobit, kB = Kilobyte. Gb = Gigabit, GB = Gigabyte.
Peter Lawrey
@PeterLawrey Großschreiben wir das 'k' in 'kb' und 'kB', also gibt es Symmetrie zu 'Gb' und 'GB'? Diese Dinge nerven mich.
Jack
3
@ Jack Es gibt eine K= 1024 und k= 1000 .;) En.wikipedia.org/wiki/Kibibyte
Peter Lawrey
9

Es gibt zwei Arten von Threads:

  1. Richtige Threads : Dies sind Abstraktionen rund um die Threading-Funktionen des zugrunde liegenden Betriebssystems. Die Thread-Erstellung ist daher genauso teuer wie die des Systems - es gibt immer einen Overhead.

  2. "Grüne" Threads : Von der JVM erstellt und geplant, sind diese billiger, aber es tritt kein richtiger Paralellismus auf. Diese verhalten sich wie Threads, werden jedoch im JVM-Thread des Betriebssystems ausgeführt. Sie werden meines Wissens nicht oft verwendet.

Der größte Faktor, den ich beim Overhead der Thread-Erstellung berücksichtigen kann, ist die Stapelgröße, die Sie für Ihre Threads definiert haben. Die Thread-Stapelgröße kann beim Ausführen der VM als Parameter übergeben werden.

Abgesehen davon hängt die Thread-Erstellung hauptsächlich vom Betriebssystem und sogar von der VM-Implementierung ab.

Lassen Sie mich jetzt auf etwas hinweisen: Das Erstellen von Threads ist teuer, wenn Sie 2000 Threads pro Sekunde pro Sekunde Ihrer Laufzeit auslösen möchten. Die JVM ist nicht dafür ausgelegt . Wenn Sie ein paar Stallarbeiter haben, die nicht immer wieder gefeuert und getötet werden, entspannen Sie sich.

Slezica
quelle
19
"... ein paar Stallarbeiter, die nicht entlassen und getötet werden ..." Warum habe ich angefangen, über die Arbeitsbedingungen nachzudenken? :-)
Stephen C
7

Das Erstellen Threadserfordert die Zuweisung einer angemessenen Menge an Speicher, da nicht nur ein, sondern zwei neue Stapel erstellt werden müssen (einer für Java-Code, einer für nativen Code). Durch die Verwendung von Executors / Thread-Pools kann der Overhead vermieden werden, indem Threads für mehrere Aufgaben für Executor wiederverwendet werden .

Philip JF
quelle
@ Raedwald, was ist das JVM, das separate Stapel verwendet?
Bestsss
1
Philip JP sagt 2 Stapel.
Raedwald
Soweit ich weiß, weisen alle JVMs zwei Stapel pro Thread zu. Für die Garbage Collection ist es hilfreich, Java-Code (auch wenn er JITed ist) anders zu behandeln als Free Casting. C.
Philip JF
@Philip JF Kannst du das bitte näher erläutern? Was meinst du mit 2 Stapeln, einem für Java-Code und einem für nativen Code? Was tut es?
Gurinder
"Soweit ich weiß, weisen alle JVMs zwei Stapel pro Thread zu." - Ich habe noch nie Beweise dafür gesehen. Vielleicht verstehen Sie die wahre Natur des Opstacks in der JVM-Spezifikation falsch. (Es ist eine Möglichkeit, das Verhalten von Bytecodes zu modellieren, nicht etwas, das zur Laufzeit verwendet werden muss, um sie auszuführen.)
Stephen C
1

Der Kern der Frage ist natürlich, was "teuer" bedeutet.

Ein Thread muss einen Stapel erstellen und den Stapel basierend auf der Ausführungsmethode initialisieren.

Es muss Kontrollstatusstrukturen einrichten, dh in welchem ​​Zustand es ausgeführt werden kann, wartet usw.

Es gibt wahrscheinlich viel Synchronisation beim Einrichten dieser Dinge.

MeBigFatGuy
quelle