Kapan kumpulan utas digunakan?

104

Jadi saya memiliki pemahaman tentang cara kerja Node.js: ia memiliki utas pendengar tunggal yang menerima acara dan kemudian mendelegasikannya ke kumpulan pekerja. Rangkaian pekerja memberi tahu pendengar setelah menyelesaikan pekerjaan, dan pendengar kemudian mengembalikan respons ke pemanggil.

Pertanyaan saya adalah ini: jika saya menjalankan server HTTP di Node.js dan memanggil sleep di salah satu peristiwa jalur yang dirutekan (seperti "/ test / sleep"), seluruh sistem akan berhenti. Bahkan utas pendengar tunggal. Tetapi pemahaman saya adalah bahwa kode ini terjadi di kumpulan pekerja.

Sekarang, sebaliknya, ketika saya menggunakan Mongoose untuk berbicara dengan MongoDB, pembacaan DB adalah operasi I / O yang mahal. Node tampaknya dapat mendelegasikan pekerjaan ke utas dan menerima callback ketika selesai; waktu yang dibutuhkan untuk memuat dari DB tampaknya tidak menghalangi sistem.

Bagaimana cara Node.js memutuskan untuk menggunakan utas kumpulan utas vs utas pendengar? Mengapa saya tidak dapat menulis kode peristiwa yang tidur dan hanya memblokir utas kumpulan utas?

Haney
sumber
@Tobi - Saya telah melihat itu. Itu masih belum menjawab pertanyaan saya. Jika pekerjaan berada di utas lain, tidur hanya akan memengaruhi utas itu dan bukan pendengar juga.
Haney
8
Sebuah pertanyaan yang tulus, di mana Anda mencoba memahami sesuatu sendiri, dan ketika Anda tidak dapat menemukan jalan keluar ke labirin, Anda meminta bantuan.
Rafael Eyng

Jawaban:

241

Pemahaman Anda tentang cara kerja node tidak benar ... tetapi ini adalah kesalahpahaman umum, karena realitas situasinya sebenarnya cukup kompleks, dan biasanya diringkas menjadi frasa kecil yang bernas seperti "node is single threaded" yang terlalu menyederhanakan berbagai hal .

Untuk saat ini, kami akan mengabaikan multi-pemrosesan / multi-threading eksplisit melalui cluster dan webworker-threads , dan hanya berbicara tentang node non-threaded yang khas.

Node berjalan dalam satu event loop. Itu satu utas, dan Anda hanya mendapatkan satu utas itu. Semua javascript yang Anda tulis dijalankan dalam loop ini, dan jika operasi pemblokiran terjadi dalam kode itu, maka itu akan memblokir seluruh loop dan tidak ada hal lain yang akan terjadi sampai selesai. Ini biasanya sifat node berulir tunggal yang sering Anda dengar. Tapi, ini bukan gambaran keseluruhan.

Fungsi dan modul tertentu, biasanya ditulis dalam C / C ++, mendukung I / O asinkron. Saat Anda memanggil fungsi dan metode ini, mereka secara internal mengelola penerusan panggilan ke thread pekerja. Misalnya, saat Anda menggunakan fsmodul untuk meminta file, fsmodul meneruskan panggilan itu ke thread pekerja, dan pekerja itu menunggu responsnya, yang kemudian disajikan kembali ke loop peristiwa yang telah berputar tanpa itu di sementara itu. Semua ini dipisahkan dari Anda, pengembang node, dan beberapa di antaranya dipisahkan dari pengembang modul melalui penggunaan libuv .

Seperti yang ditunjukkan oleh Denis Dollfus di komentar (dari jawaban ini untuk pertanyaan serupa), strategi yang digunakan oleh libuv untuk mencapai asynchronous I / O tidak selalu merupakan kumpulan utas, khususnya dalam kasus httpmodul, tampaknya strategi yang berbeda digunakan saat ini. Untuk tujuan kita di sini, penting untuk diperhatikan bagaimana konteks asinkron dicapai (dengan menggunakan libuv) dan bahwa kumpulan utas yang dikelola oleh libuv adalah salah satu dari beberapa strategi yang ditawarkan oleh pustaka tersebut untuk mencapai asinkronitas.


Pada garis singgung yang sebagian besar terkait, ada analisis yang jauh lebih dalam tentang bagaimana node mencapai asinkronitas, dan beberapa masalah potensial terkait dan cara mengatasinya, dalam artikel yang sangat bagus ini . Sebagian besar memperluas apa yang saya tulis di atas, tetapi selain itu menunjukkan:

  • Modul eksternal apa pun yang Anda sertakan dalam proyek Anda yang menggunakan C ++ asli dan libuv kemungkinan besar akan menggunakan kumpulan thread (pikirkan: akses database)
  • libuv memiliki ukuran kumpulan utas default 4, dan menggunakan antrean untuk mengelola akses ke kumpulan utas - hasilnya adalah jika Anda memiliki 5 kueri DB yang berjalan lama yang semuanya berjalan pada waktu yang sama, salah satunya (dan asinkron lainnya tindakan yang bergantung pada kumpulan utas) akan menunggu kueri tersebut selesai bahkan sebelum mereka memulai
  • Anda dapat menguranginya dengan meningkatkan ukuran kumpulan utas melalui UV_THREADPOOL_SIZEvariabel lingkungan, selama Anda melakukannya sebelum kumpulan utas diperlukan dan dibuat:process.env.UV_THREADPOOL_SIZE = 10;

Jika Anda menginginkan multi-processing atau multi-threading in node tradisional, Anda bisa mendapatkannya melalui clustermodul bawaan atau berbagai modul lain seperti yang disebutkan di atas webworker-threads, atau Anda dapat memalsukannya dengan menerapkan beberapa cara memotong pekerjaan Anda dan secara manual menggunakan setTimeoutatau setImmediateatauprocess.nextTick untuk menghentikan sementara pekerjaan Anda dan melanjutkannya di loop berikutnya untuk membiarkan proses lain selesai (tapi itu tidak disarankan).

Harap dicatat, jika Anda menulis kode yang berjalan lama / memblokir di javascript, Anda mungkin membuat kesalahan. Bahasa lain akan bekerja jauh lebih efisien.

Jason
sumber
1
Sialan, ini benar-benar menjelaskannya untukku. Terima kasih banyak @Jason!
Haney
5
Tidak masalah :) Saya menemukan diri saya di mana Anda belum lama ini, dan sulit untuk mendapatkan jawaban yang terdefinisi dengan baik karena di satu sisi Anda memiliki pengembang C / C ++ yang jawabannya sudah jelas, dan di sisi lain Anda memiliki tipikal pengembang web yang belum pernah mempelajari pertanyaan semacam ini sebelumnya. Saya bahkan tidak yakin jawaban saya 100% secara teknis benar ketika Anda turun ke level C, tetapi tepat dalam garis besar.
Jason
3
Menggunakan kumpulan utas untuk permintaan jaringan akan menjadi pemborosan sumber daya yang sangat besar. Menurut pertanyaan ini "Ia melakukan jaringan asinkron I / O berdasarkan antarmuka I / O asinkron di platform yang berbeda, seperti epoll, kqueue dan IOCP, tanpa kumpulan thread" - yang masuk akal.
Denis Dollfus
1
... yang mengatakan, jika Anda melakukan beberapa pekerjaan berat di utas javascript utama secara langsung, atau Anda tidak memiliki cukup sumber daya atau tidak mengelolanya dengan tepat untuk memberikan ruang kepala yang cukup ke threadpool, Anda dapat menyebabkan kelambatan pada konkurensi yang lebih rendah ambang batas - hasilnya adalah, untuk sumber daya sistem yang sama, Anda biasanya akan mengalami thruput yang lebih tinggi dengan node.js daripada dengan opsi lain (meskipun ada sistem berbasis peristiwa lain dalam bahasa lain yang bertujuan untuk menantang itu - saya belum melihat tolok ukur terbaru meskipun) - jelas bahwa model berbasis peristiwa mengungguli model berulir.
Jason
1
@Aabid Thread listener tidak mengeksekusi kueri database, jadi akan membutuhkan sekitar 6 detik untuk menyelesaikan semua 10 kueri tersebut (dengan ukuran kumpulan thread default 4). Jika Anda perlu melakukan pekerjaan apa pun dalam javascript yang tidak memerlukan hasil dari kueri database tersebut untuk diselesaikan, misalnya lebih banyak permintaan masuk yang tidak memerlukan pekerjaan asinkron apa pun untuk diselesaikan oleh kumpulan utas, itu akan terus bekerja di utama putaran acara.
Jason
20

Jadi saya memiliki pemahaman tentang cara kerja Node.js: ia memiliki utas pendengar tunggal yang menerima acara dan kemudian mendelegasikannya ke kumpulan pekerja. Rangkaian pekerja memberi tahu pendengar setelah menyelesaikan pekerjaan, dan pendengar kemudian mengembalikan respons ke pemanggil.

Ini tidak terlalu akurat. Node.js hanya memiliki satu utas "pekerja" yang melakukan eksekusi javascript. Ada thread dalam node yang menangani pemrosesan IO, tetapi menganggapnya sebagai "pekerja" adalah kesalahpahaman. Sebenarnya hanya ada penanganan IO dan beberapa detail lain dari implementasi internal node, tetapi sebagai programmer Anda tidak dapat mempengaruhi perilaku mereka selain beberapa parameter misc seperti MAX_LISTENERS.

Pertanyaan saya adalah ini: jika saya menjalankan server HTTP di Node.js dan memanggil sleep di salah satu peristiwa jalur yang dirutekan (seperti "/ test / sleep"), seluruh sistem akan berhenti. Bahkan utas pendengar tunggal. Tetapi pemahaman saya adalah bahwa kode ini terjadi di kumpulan pekerja.

Tidak ada mekanisme tidur dalam JavaScript. Kami dapat membahas ini secara lebih konkrit jika Anda memposting potongan kode yang menurut Anda berarti "tidur". Tidak ada fungsi yang dipanggil untuk mensimulasikan sesuatu seperti time.sleep(30)di python, misalnya. Ada setTimeouttapi itu pada dasarnya BUKAN tidur. setTimeoutdan setIntervalsecara eksplisit melepaskan , bukan memblokir, loop peristiwa sehingga bit kode lain dapat dieksekusi pada thread eksekusi utama. Satu-satunya hal yang dapat Anda lakukan adalah melakukan loop sibuk pada CPU dengan komputasi dalam memori, yang memang akan membuat thread eksekusi utama kelaparan dan membuat program Anda tidak responsif.

Bagaimana cara Node.js memutuskan untuk menggunakan utas kumpulan utas vs utas pendengar? Mengapa saya tidak dapat menulis kode peristiwa yang tidur dan hanya memblokir utas kumpulan utas?

Jaringan IO selalu asinkron. Akhir dari cerita. Disk IO memiliki API sinkron dan asinkron, jadi tidak ada "keputusan". node.js akan berperilaku sesuai dengan fungsi inti API yang Anda panggil sync vs async normal. Sebagai contoh: fs.readFilevs fs.readFileSync. Untuk proses anak, ada juga yang terpisah child_process.execdanchild_process.execSync API .

Aturan praktis selalu menggunakan API asinkron. Alasan yang valid untuk menggunakan API sinkronisasi adalah untuk kode inisialisasi dalam layanan jaringan sebelum mendengarkan koneksi atau dalam skrip sederhana yang tidak menerima permintaan jaringan untuk alat build dan sejenisnya.

Peter Lyons
sumber
1
Dari manakah asynchronous API ini berasal? Saya mengerti apa yang Anda katakan, tetapi siapa pun yang menulis API ini memilih ke IOCP / async. Bagaimana mereka memilih untuk melakukan ini?
Haney
3
Pertanyaannya adalah bagaimana dia akan menulis kode intensif waktunya sendiri dan bukan memblokir.
Jason
1
Iya. Node menyediakan jaringan UDP, TCP, dan HTTP dasar. Ini HANYA menyediakan API "berbasis kumpulan" asinkron. Semua kode node.js di dunia tanpa kecuali menggunakan API asinkron berbasis kumpulan ini karena hanya ada semua yang tersedia. Sistem file dan proses anak adalah cerita yang berbeda, tetapi jaringan secara konsisten tidak sinkron.
Peter Lyons
4
Hati-hati, Peter, jangan sampai kamu menjadi pepatah pot untuk ketelnya. Dia ingin tahu bagaimana penulis API jaringan melakukannya, bukan bagaimana orang yang menggunakan API jaringan melakukannya. Saya akhirnya memperoleh pemahaman tentang bagaimana node berperilaku re: peristiwa non-pemblokiran karena saya ingin menulis kode non-pemblokiran saya sendiri yang tidak ada hubungannya dengan jaringan atau API asinkron bawaan lainnya. Jelas David ingin melakukan hal yang sama.
Jason
2
Node tidak menggunakan kumpulan utas untuk IO, ia menggunakan IO non-pemblokiran asli, satu-satunya pengecualian adalah fs, sejauh yang saya tahu
vkurchatkin
2

Kumpulan benang bagaimana kapan dan siapa yang digunakan:

Pertama ketika kita menggunakan / menginstal Node di komputer, itu memulai proses antara proses lain yang disebut proses node di komputer, dan itu terus berjalan sampai Anda mematikannya. Dan proses yang berjalan ini adalah apa yang kami sebut sebagai utas tunggal.

masukkan deskripsi gambar di sini

Jadi mekanisme utas tunggal memudahkan untuk memblokir aplikasi node tetapi ini adalah salah satu fitur unik yang dibawa Node.js ke tabel. Jadi, sekali lagi jika Anda menjalankan aplikasi node Anda, itu akan berjalan hanya dalam satu thread. Tidak masalah jika Anda memiliki 1 atau jutaan pengguna yang mengakses aplikasi Anda pada saat yang bersamaan.

Jadi mari kita pahami apa yang terjadi di single thread nodejs saat Anda memulai aplikasi node. Pada awalnya program diinisialisasi, kemudian semua kode tingkat atas dijalankan, yang berarti semua kode yang tidak ada di dalam fungsi panggilan balik ( ingat semua kode di dalam semua fungsi panggilan balik akan dieksekusi di bawah loop peristiwa ).

Setelah itu, semua kode modul dieksekusi kemudian daftarkan semua callback, akhirnya event loop dimulai untuk aplikasi Anda.

masukkan deskripsi gambar di sini

Jadi seperti yang kita bahas sebelumnya, semua fungsi dan kode callback di dalam fungsi tersebut akan dijalankan di bawah event loop. Dalam event loop, beban didistribusikan dalam fase yang berbeda. Bagaimanapun, saya tidak akan membahas tentang event loop di sini.

Nah untuk pemahaman yang lebih baik tentang Thread pool I meminta Anda untuk membayangkan bahwa dalam event loop, kode di dalam satu fungsi callback dijalankan setelah menyelesaikan eksekusi kode di dalam fungsi callback lain, sekarang jika ada beberapa tugas yang sebenarnya terlalu berat. Mereka kemudian akan memblokir utas tunggal nodejs kami. Jadi, di situlah kumpulan utas masuk, yang seperti loop peristiwa, disediakan untuk Node.js oleh perpustakaan libuv.

Jadi kumpulan utas bukan bagian dari nodej itu sendiri, itu disediakan oleh libuv untuk memindahkan tugas berat ke libuv, dan libuv akan mengeksekusi kode-kode itu di utasnya sendiri dan setelah eksekusi libuv akan mengembalikan hasilnya ke acara di loop acara.

masukkan deskripsi gambar di sini

Kumpulan benang memberi kita empat utas tambahan, yang benar-benar terpisah dari utas tunggal utama. Dan kami benar-benar dapat mengkonfigurasinya hingga 128 utas.

Jadi semua utas ini bersama-sama membentuk kumpulan utas. dan event loop kemudian dapat secara otomatis memindahkan tugas berat ke kumpulan thread.

Bagian yang menyenangkan adalah semua ini terjadi secara otomatis di belakang layar. Bukan kami pengembang yang memutuskan apa yang masuk ke kumpulan utas dan apa yang tidak.

Ada banyak tugas yang masuk ke kumpulan utas, seperti

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups
Tuan
sumber
0

Kesalahpahaman ini hanyalah perbedaan antara multi-tasking pre-emptive dan multitasking kooperatif ...

Tidur mematikan seluruh karnaval karena sebenarnya hanya ada satu baris untuk semua wahana, dan Anda menutup gerbangnya. Anggap saja sebagai "juru bahasa JS dan beberapa hal lainnya" dan abaikan utasnya ... untuk Anda, hanya ada satu utas, ...

... jadi jangan memblokirnya.

Gregory R. Sudderth
sumber