Mengapa membuat Thread dikatakan mahal?

180

Tutorial Java mengatakan bahwa membuat Thread adalah mahal. Tapi mengapa itu mahal? Apa sebenarnya yang terjadi ketika Java Thread dibuat yang membuat pembuatannya mahal? Saya menganggap pernyataan itu benar, tetapi saya hanya tertarik pada mekanisme pembuatan Thread di JVM.

Overhead siklus hidup ulir. Pembuatan dan penguraian benang tidak gratis. Overhead aktual bervariasi di seluruh platform, tetapi pembuatan utas membutuhkan waktu, memperkenalkan latensi ke dalam pemrosesan permintaan, dan memerlukan beberapa aktivitas pemrosesan oleh JVM dan OS. Jika permintaan sering dan ringan, seperti di sebagian besar aplikasi server, membuat utas baru untuk setiap permintaan dapat menggunakan sumber daya komputasi yang signifikan.

Dari Java Concurrency in Practice
Oleh Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Cetak ISBN-10: 0-321-34960-1

kachanov
sumber
Saya tidak tahu konteks tutorial yang Anda baca mengatakan ini: apakah ini menyiratkan bahwa kreasi itu sendiri mahal, atau bahwa "membuat utas" mahal. Perbedaan yang saya coba tunjukkan adalah antara tindakan murni membuat utas (sebut saja instantiating atau sesuatu), atau fakta bahwa Anda memiliki utas (jadi menggunakan utas: jelas memiliki overhead). Yang mana yang diklaim // yang mana yang ingin Anda tanyakan?
Nanne
9
@typoknig - Mahal dibandingkan dengan TIDAK membuat utas baru :)
willcodejavaforfood
kemungkinan duplikat overhead pembuatan utas Java
Paul Draper
1
threadpools untuk menang. tidak perlu selalu membuat utas baru untuk tugas.
Alexander Mills

Jawaban:

149

Pembuatan utas Java mahal karena ada sedikit pekerjaan yang harus dilakukan:

  • Blok memori yang besar harus dialokasikan dan diinisialisasi untuk tumpukan ulir.
  • Panggilan sistem perlu dibuat untuk membuat / mendaftarkan utas asli dengan OS host.
  • Deskriptor perlu dibuat, diinisialisasi dan ditambahkan ke struktur data internal JVM.

Ini juga mahal dalam arti bahwa utas mengikat sumber daya selama masih hidup; misal tumpukan ulir, benda apa pun yang dapat dijangkau dari tumpukan, penjelas utas JVM, penjelas utas asli OS.

Biaya semua hal ini adalah platform spesifik, tetapi tidak murah untuk platform Java apa pun yang pernah saya temui.


Pencarian Google menemukan saya patokan lama yang melaporkan tingkat pembuatan utas ~ 4000 per detik pada Sun Java 1.4.1 pada prosesor ganda 2002 Xeon yang menjalankan Linux kuno 2002. Platform yang lebih modern akan memberikan angka yang lebih baik ... dan saya tidak bisa mengomentari metodologi ... tapi setidaknya itu memberikan gambaran kasar tentang seberapa mahal kemungkinan pembuatan utas.

Benchmark Peter Lawrey menunjukkan bahwa pembuatan utas secara signifikan lebih cepat akhir-akhir ini dalam hal absolut, tetapi tidak jelas berapa banyak dari ini karena peningkatan di Jawa dan / atau OS ... atau kecepatan prosesor yang lebih tinggi. Tetapi angka-angkanya masih menunjukkan peningkatan 150+ lipat jika Anda menggunakan kumpulan utas versus membuat / memulai utas baru setiap kali. (Dan dia menekankan bahwa ini semua relatif ...)


(Di atas mengasumsikan "utas asli" daripada "utas hijau", tetapi JVM modern semua menggunakan utas asli untuk alasan kinerja. Utas hijau mungkin lebih murah untuk dibuat, tetapi Anda membayarnya di area lain.)


Saya telah melakukan sedikit penggalian untuk melihat bagaimana tumpukan Java thread benar-benar dialokasikan. Dalam kasus OpenJDK 6 di Linux, tumpukan thread dialokasikan oleh panggilan ke pthread_createyang membuat utas asli. (JVM tidak melewati pthread_createtumpukan yang telah dialokasikan sebelumnya.)

Kemudian, di pthread_createdalam stack dialokasikan oleh panggilan ke mmapsebagai berikut:

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

Menurutnya man mmap, MAP_ANONYMOUSflag menyebabkan memori diinisialisasi ke nol.

Dengan demikian, meskipun mungkin tidak penting bahwa tumpukan ulir Java baru dipusatkan (sesuai spesifikasi JVM), dalam praktiknya (setidaknya dengan OpenJDK 6 di Linux), tumpukan ini nol.

Stephen C
sumber
2
@Raedwald - itu adalah bagian inisialisasi yang mahal. Di suatu tempat, sesuatu (misalnya GC, atau OS) akan nol byte sebelum blok diubah menjadi tumpukan thread. Itu membutuhkan siklus memori fisik pada perangkat keras biasa.
Stephen C
2
"Di suatu tempat, sesuatu (misalnya GC, atau OS) akan nol byte". Itu akan? OS akan melakukannya jika memerlukan alokasi halaman memori baru, untuk alasan keamanan. Tapi itu tidak biasa. Dan OS mungkin menyimpan cache dari halaman yang sudah nol-ed (IIRC, Linux melakukannya). Mengapa GC mengganggu, mengingat bahwa JVM akan mencegah program Java membaca kontennya? Perhatikan bahwa malloc()fungsi C standar , yang mungkin digunakan JVM dengan baik, tidak menjamin bahwa memori yang dialokasikan nol-ed (mungkin untuk menghindari masalah kinerja seperti itu saja).
Raedwald
1
stackoverflow.com/questions/2117072/... setuju bahwa "Salah satu faktor utama adalah memori tumpukan yang dialokasikan untuk setiap utas".
Raedwald
2
@Raedwald - lihat jawaban yang diperbarui untuk info tentang bagaimana tumpukan sebenarnya dialokasikan.
Stephen C
2
Mungkin (bahkan mungkin) bahwa halaman memori yang dialokasikan oleh mmap()panggilan tersebut dipetakan secara copy-on-write ke halaman nol, sehingga initailisasinya terjadi bukan di dalam mmap()dirinya sendiri, tetapi ketika halaman pertama kali ditulis , dan kemudian hanya satu halaman pada saat itu. sebuah waktu. Yaitu, ketika utas memulai eksekusi, dengan biaya ditanggung oleh utas yang dibuat daripada utas pencipta.
Raedwald
76

Yang lain telah mendiskusikan dari mana biaya threading berasal. Jawaban ini mencakup mengapa membuat utas tidak semahal dibandingkan banyak operasi, tetapi relatif mahal dibandingkan dengan alternatif pelaksanaan tugas, yang relatif lebih murah.

Alternatif yang paling jelas untuk menjalankan tugas di utas lain adalah menjalankan tugas di utas yang sama. Ini sulit dipahami oleh mereka yang beranggapan bahwa lebih banyak utas selalu lebih baik. Logikanya adalah jika overhead menambahkan tugas ke utas lain lebih besar dari waktu yang Anda simpan, bisa lebih cepat untuk melakukan tugas di utas saat ini.

Alternatif lain adalah menggunakan thread pool. Kumpulan utas dapat lebih efisien karena dua alasan. 1) menggunakan kembali utas yang sudah dibuat. 2) Anda dapat menyetel / mengontrol jumlah utas untuk memastikan Anda memiliki kinerja optimal.

Program berikut mencetak ....

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

Ini adalah tes untuk tugas sepele yang memperlihatkan overhead dari setiap opsi threading. (Tugas pengujian ini adalah jenis tugas yang sebenarnya paling baik dilakukan di utas saat ini.)

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

Seperti yang Anda lihat, membuat utas baru hanya berharga ~ 70 μs. Ini bisa dianggap sepele di banyak, jika tidak sebagian besar, kasus penggunaan. Secara relatif, ini lebih mahal daripada alternatifnya dan untuk beberapa situasi, thread thread atau tidak menggunakan thread sama sekali adalah solusi yang lebih baik.

Peter Lawrey
sumber
8
Itu kode yang bagus di sana. Ringkas, to the point dan dengan jelas menampilkan isinya.
Nicholas
Pada blok terakhir, saya yakin hasilnya miring, karena pada dua blok pertama, utas utama dilepas secara paralel ketika pekerja menempatkan benang. Namun di blok terakhir, tindakan pengambilan semua dilakukan secara seri, sehingga melebarkan nilainya. Anda mungkin dapat menggunakan queue.clear () dan menggunakan CountDownLatch sebagai gantinya untuk menunggu utas selesai.
Victor Grazi
@ ViktorGrazi Saya berasumsi Anda ingin mengumpulkan hasilnya secara terpusat. Ia melakukan jumlah pekerjaan antrian yang sama di setiap kasus. Kait hitung mundur akan sedikit lebih cepat.
Peter Lawrey
Sebenarnya, mengapa tidak melakukannya saja secara cepat, seperti menambah penghitung; letakkan seluruh BlockingQueue. Periksa penghitung di akhir untuk mencegah kompiler mengoptimalkan operasi kenaikan
Victor Grazi
@ Grazi Anda bisa melakukan itu dalam kasus ini tetapi Anda tidak akan dalam kebanyakan kasus realistis karena menunggu di meja mungkin tidak efisien. Jika Anda melakukannya, perbedaan antara contoh akan lebih besar.
Peter Lawrey
31

Secara teori, ini tergantung pada JVM. Dalam praktiknya, setiap utas memiliki jumlah memori tumpukan yang relatif besar (256 KB per default, saya kira). Selain itu, utas diimplementasikan sebagai utas OS, sehingga membuatnya melibatkan panggilan OS, yaitu saklar konteks.

Sadarilah bahwa "mahal" dalam komputasi selalu sangat relatif. Penciptaan utas sangat mahal relatif terhadap penciptaan sebagian besar objek, tetapi tidak terlalu mahal relatif terhadap pencarian harddisk acak. Anda tidak harus menghindari membuat utas dengan cara apa pun, tetapi membuat ratusannya per detik bukanlah langkah yang cerdas. Dalam kebanyakan kasus, jika desain Anda membutuhkan banyak utas, Anda harus menggunakan kumpulan utas ukuran terbatas.

Michael Borgwardt
sumber
9
Btw kb = kilo-bit, kB = kilo byte. Gb = giga bit, GB = giga byte.
Peter Lawrey
@PeterLawrey kita menggunakan huruf besar 'k' di 'kb' dan 'kB', jadi ada simetri untuk 'Gb' dan 'GB'? Hal-hal ini menggangguku.
Jack
3
@Jack Ada a K= 1024 dan k= 1000.;) en.wikipedia.org/wiki/Kibibyte
Peter Lawrey
9

Ada dua jenis utas:

  1. Thread yang tepat : ini adalah abstraksi di sekitar fasilitas threading sistem operasi yang mendasarinya. Karenanya, pembuatan utas sama mahalnya dengan sistem - selalu ada overhead.

  2. Thread "Hijau" : dibuat dan dijadwalkan oleh JVM, ini lebih murah, tetapi tidak terjadi paralellisme yang tepat. Ini berperilaku seperti utas, tetapi dieksekusi dalam utas JVM di OS. Mereka tidak sering digunakan, setahu saya.

Faktor terbesar yang dapat saya pikirkan dalam overhead pembuatan thread, adalah ukuran stack yang telah Anda tentukan untuk thread Anda. Thread stack-size dapat dilewatkan sebagai parameter saat menjalankan VM.

Selain itu, pembuatan utas sebagian besar bergantung pada OS, dan bahkan tergantung pada implementasi VM.

Sekarang, izinkan saya menunjukkan sesuatu: membuat utas mahal jika Anda berencana menembakkan 2.000 utas per detik, setiap detik dari runtime Anda. JVM tidak dirancang untuk mengatasinya . Jika Anda akan memiliki beberapa pekerja stabil yang tidak akan dipecat dan dibunuh berulang kali, santai saja.

slezica
sumber
19
"... beberapa pekerja stabil yang tidak akan dipecat dan dibunuh ..." Mengapa saya mulai berpikir tentang kondisi tempat kerja? :-)
Stephen C
6

Menciptakan Threadsmembutuhkan alokasi memori yang cukup karena harus membuat bukan hanya satu, tetapi dua tumpukan baru (satu untuk kode java, satu untuk kode asli). Penggunaan Executors / Thread Pools dapat menghindari overhead, dengan menggunakan kembali utas untuk beberapa tugas untuk Pelaksana .

Philip JF
sumber
@ Raedwald, apa jvm yang menggunakan tumpukan terpisah?
bestsss
1
Philip JP mengatakan 2 tumpukan.
Raedwald
Sejauh yang saya tahu, semua JVM mengalokasikan dua tumpukan per utas. Sangat membantu bagi pengumpulan sampah untuk memperlakukan kode Java (bahkan ketika JITed) berbeda dari casting bebas c.
Philip JF
@ Philip JF Bisakah Anda jelaskan? Apa yang Anda maksud dengan 2 tumpukan satu untuk kode Java dan satu untuk kode asli? Apa fungsinya?
Gurinder
"Sejauh yang saya tahu, semua JVM mengalokasikan dua tumpukan per utas." - Saya belum pernah melihat bukti yang akan mendukung ini. Mungkin Anda salah memahami sifat sebenarnya dari opstack dalam spesifikasi JVM. (Ini adalah cara memodelkan perilaku bytecodes, bukan sesuatu yang perlu digunakan saat runtime untuk mengeksekusinya.)
Stephen C
1

Jelas inti masalahnya adalah apa yang dimaksud dengan 'mahal'.

Utas perlu membuat tumpukan dan menginisialisasi tumpukan berdasarkan metode jalankan.

Itu perlu mengatur struktur status kontrol, yaitu keadaan apa yang bisa dijalankan, menunggu dll.

Mungkin ada banyak sinkronisasi di sekitar pengaturan hal-hal ini.

MeBigFatGuy
sumber