Multithreading di Node.js. Perulangan acara

Infa akan bermanfaat bagi pengembang JS yang ingin memahami esensi dari bekerja dengan Node.js dan Event Loop. Anda dapat secara sadar dan lebih fleksibel mengontrol aliran program (web-server).


Saya menyusun artikel ini berdasarkan laporan terakhir saya kepada kolega.
Di akhir artikel ada bahan yang berguna untuk studi independen.


Bagaimana Node.js. Fitur Asinkron


Mari kita lihat kode ini: ini menunjukkan dengan sempurna sinkronisasi eksekusi kode di Node.js. Permintaan dibuat di suatu tempat di GitHub, lalu file dibaca dan hasilnya ditampilkan di konsol. Apa yang jelas dari kode sinkron ini?


gambar


Misalkan ini adalah server web abstrak yang melakukan operasi pada router. Jika permintaan masuk tiba di router ini, kami membuat permintaan lebih lanjut, membaca file, dan mencetaknya ke konsol. Dengan demikian, waktu yang dihabiskan untuk meminta dan membaca file, server akan diblokir, itu tidak akan dapat memproses permintaan masuk lainnya, juga tidak akan melakukan operasi lain.


Apa sajakah pilihan untuk menyelesaikan masalah ini?


  1. Multithreading
  2. I / O Non-Blok

Untuk opsi pertama (multithreading) ada contoh yang baik dengan server web Apache vs Nginx.


gambar


Sebelumnya, Apache menaikkan aliran untuk setiap permintaan yang masuk: ada berapa permintaan, jumlah utas yang sama. Pada saat ini, Nginx memiliki keuntungan menggunakan I / O yang tidak menghalangi. Di sini Anda dapat melihat bahwa dengan peningkatan jumlah permintaan masuk, jumlah memori yang dikonsumsi oleh Apache meningkat, dan pada slide berikutnya, jumlah permintaan yang diproses per detik dengan jumlah koneksi untuk Nginx lebih tinggi.


gambar


Jelas ditunjukkan bahwa input / output non-blocking lebih efisien.


Input / output non-blocking dimungkinkan berkat sistem operasi modern yang menyediakan mekanisme ini - sebuah event demultiplexer.


Demultiplexer adalah mekanisme yang menerima permintaan dari suatu aplikasi, mendaftarkannya, dan mengeksekusinya.


gambar


Di bagian atas diagram terlihat bahwa kita memiliki aplikasi dan operasi dilakukan di dalamnya (biarkan membaca file). Untuk melakukan ini, permintaan dibuat ke demultiplexer acara, sumber daya dikirim ke sini (tautan ke file), operasi dan panggilan balik yang diinginkan. Event demultiplexer mendaftarkan permintaan ini dan mengembalikan kontrol langsung ke aplikasi - dengan demikian, itu tidak diblokir. Kemudian ia melakukan operasi pada file, dan setelah itu, ketika file dibaca, callback terdaftar dalam antrian eksekusi. Kemudian Perulangan Peristiwa secara bertahap menyinkronkan setiap panggilan balik dari antrian ini. Dan, karenanya, mengembalikan hasilnya ke aplikasi. Selanjutnya (jika perlu) semuanya dilakukan lagi.


Dengan demikian, berkat I / O non-pemblokiran ini, Node.js dapat asinkron.


Saya akan mengklarifikasi bahwa dalam hal ini, itu adalah sistem operasi yang memberi kami input / output yang tidak menghalangi. Untuk non-memblokir input / output (umumnya, pada prinsipnya, untuk operasi input / output) kami menyertakan permintaan jaringan dan bekerja dengan file.


Ini adalah konsep umum I / O yang tidak menghalangi. Ketika peluang muncul, Ryan Dahl, pengembang Node.js, terinspirasi oleh pengalaman Nginx, yang menggunakan I / O non-blocking, dan memutuskan untuk membuat platform khusus untuk pengembang. Hal pertama yang perlu dia lakukan adalah β€œberteman” dengan platformnya dengan demultiplexer acara. Masalahnya adalah bahwa demultiplexer diimplementasikan secara berbeda di setiap sistem operasi, dan ia harus menulis pembungkus, yang kemudian dikenal sebagai libuv. Ini adalah perpustakaan yang ditulis dalam C. Ini menyediakan antarmuka tunggal untuk bekerja dengan demultiplexer acara ini.


Fitur perpustakaan Libuv


gambar


Di Linux, pada prinsipnya, saat ini, semua operasi dengan file lokal diblokir. Artinya, sepertinya ada input / output non-blocking, tetapi justru ketika bekerja dengan file lokal bahwa operasi masih memblokir. Itulah sebabnya libuv menggunakan utas secara internal untuk meniru I / O yang tidak menghalangi. 4 utas muncul dari kotak, dan di sini kita perlu membuat kesimpulan yang paling penting: jika kita melakukan beberapa 4 operasi berat pada file lokal, oleh karena itu, kita akan memblokir seluruh aplikasi kita (ini di Linux, OS lain tidak).


gambar


Pada slide ini, kita melihat arsitektur Node.js. Untuk berinteraksi dengan sistem operasi, perpustakaan libuv yang ditulis dalam C digunakan; Untuk mengkompilasi kode JavaScript ke dalam kode mesin, mesin Google V8 digunakan, ada juga perpustakaan Core Node.js, yang berisi modul untuk bekerja dengan permintaan jaringan, sistem file, dan modul untuk logging. Agar semua ini saling berinteraksi, Node.js Bindings ditulis. 4 komponen ini membentuk struktur Node.js. Mekanisme Event Loop sendiri ada di libuv.


Perulangan acara


gambar


Ini adalah representasi paling sederhana dari apa yang tampak seperti Loop Peristiwa. Ada antrian kejadian tertentu, ada siklus kejadian tanpa akhir yang secara serentak melakukan operasi dari antrian, dan mendistribusikannya lebih lanjut.


Slide ini menunjukkan bagaimana Loop Acara terlihat langsung di Node.js.
gambar


Di sana, implementasinya lebih menarik dan lebih rumit. Pada dasarnya, suatu Peristiwa Perulangan adalah perulangan peristiwa, dan itu tidak terbatas selama ada sesuatu yang harus dilakukan. Perulangan Peristiwa di Node.js dibagi menjadi beberapa fase. (Fase dari slide 8 harus dibandingkan dengan kode sumber pada slide 9.)


gambar


Fase 1 - Pengatur Waktu


Fase ini dilakukan langsung oleh Event Loop. (Cuplikan kode dengan uv_update_time) - di sini waktu ketika Perulangan Acara mulai bekerja hanya diperbarui.


uv_run_timers - dalam metode ini, tindakan penghitung waktu berikutnya dilakukan. Ada tumpukan tertentu, lebih tepatnya, sekelompok timer, ini pada dasarnya sama dengan antrian di mana timer berada. Timer dengan waktu terkecil diambil, dibandingkan dengan waktu saat ini dari Perulangan Kejadian, dan jika sudah waktunya untuk mengeksekusi timer ini, callback-nya dieksekusi. Perlu dicatat di sini bahwa Node.js memiliki implementasi setTimeout dan ada setInterval. Untuk libuv, ini pada dasarnya adalah hal yang sama, hanya setInterval yang masih memiliki flag berulang.


Dengan demikian, jika timer ini memiliki flag ulangi, maka itu lagi ditempatkan di antrian acara dan kemudian diproses dengan cara yang sama.


Fase 2 - I / O-callback


Di sini kita perlu kembali ke diagram tentang input / output yang tidak menghalangi.


Ketika demultiplexer acara membaca file dan mengantre callback, itu hanya sesuai dengan tahap I / O-callback. Di sini, callback dilakukan untuk input / output yang tidak menghalangi, yaitu fungsi-fungsi yang digunakan setelah permintaan ke database atau sumber daya lain atau untuk membaca / menulis file. Mereka dilakukan tepat pada fase ini.


Pada slide 9, eksekusi fungsi I / O-callback memulai baris 367: ran_pending = uv_run_pending (loop).


3 fase - menunggu, persiapan


Ini adalah operasi internal untuk callback, pada kenyataannya, kita tidak dapat mempengaruhi fase, hanya secara tidak langsung. Ada proses.NextTick, panggilan baliknya mungkin secara tidak sengaja dieksekusi dalam fase persiapan, menunggu. process.nextTick dieksekusi pada fase saat ini, mis., pada kenyataannya, process.nextTick dapat bekerja di fase apa pun. Tidak ada alat yang siap pakai untuk menjalankan kode dalam fase "menunggu, mempersiapkan" di Node.js.


Pada slide 9, baris 368, 369 berhubungan dengan fase ini:
uv_run_idle (loop) - tunggu;
uv_run_prepare (loop) - persiapan.


4 fase - survei


Di sinilah semua kode kami yang kami tulis di JS dieksekusi. Awalnya, semua permintaan kami dapatkan di sini, dan di sinilah Node.js dapat diblokir. Jika ada operasi perhitungan yang berat sampai di sini, maka pada tahap ini aplikasi kita mungkin akan membeku dan menunggu sampai operasi ini selesai.
Pada slide 9, fungsi polling ada di baris 370: uv_io_poll (loop, timeout).


5 fase - periksa


Ada timer setImmediate di Node.js, panggilan baliknya dijalankan pada fase ini.
Dalam kode sumber, ini adalah baris 371: uv_run_check (loop).


6 fase (terakhir) - acara callback tutup


Misalnya, soket web perlu menutup koneksi, pada fase ini panggilan balik dari acara ini akan dipanggil.


Dalam kode sumber, ini adalah baris 372: uv_run_closing_handless (loop).


Dan pada akhirnya, Event Loop Node.js adalah sebagai berikut


gambar


Pertama, dalam antrian timer, timer dieksekusi, periode yang telah mendekati.


Kemudian I / O-callback dieksekusi.


Maka kodenya adalah basis, lalu setImmediate dan tutup acara.


Setelah itu, semuanya berulang dalam satu lingkaran. Untuk mendemonstrasikan ini, saya akan membuka kodenya. Bagaimana ini akan dilakukan?


gambar


Kami tidak memiliki pengatur waktu dalam antrean, sehingga Perulangan Kejadian berlanjut. Tidak ada I / O-callback juga, jadi kami segera pergi ke tahap pemungutan suara. Semua kode yang ada di sini pada awalnya dieksekusi dalam fase polling. Karena itu, pertama kita cetak script_start, setInterval ditempatkan di antrian timer (tidak dieksekusi, hanya ditempatkan). setTimeout juga ditempatkan dalam antrian timer, dan kemudian janji dieksekusi: janji pertama 1 dan kemudian janji 2.


Pada centang selanjutnya (loop acara), kita kembali ke tahap pengatur waktu, di sini dalam antrian sudah ada 2 pengatur waktu: setInterval dan setTimeout. Keduanya 0 tertunda, masing-masing siap dieksekusi.


SetInterval dijalankan (output ke konsol), lalu setTimeout 1. Tidak ada panggilan balik I / O yang tidak menghalangi, maka akan ada fase polling, janji 3 dan janji 4 ditampilkan di konsol.


Selanjutnya, timer setTimeout dicatat. Ini mengakhiri tanda centang, pergi ke tanda centang berikutnya. Ada penghitung waktu lagi, output ke konsol diaturInterval dan setTimeout 2, kemudian janji 5 dan janji 6 ditampilkan.


Kami meninjau Perulangan Acara dan sekarang dapat berbicara lebih detail tentang multithreading.


Threading - modul worker_threads


Threading telah muncul di Node.js berkat modul worker_threads di versi 10.5. Dan dalam versi ke-10, itu diluncurkan secara eksklusif dengan - kunci pekerja eksperimental, dan dari versi ke-11 dimungkinkan untuk memulai tanpa itu.


Node.js juga memiliki modul cluster, tetapi tidak meningkatkan thread - ia memunculkan beberapa proses lagi. Skalabilitas aplikasi adalah tujuan utamanya.


gambar


Seperti apa 1 proses itu:
1 proses Node.js, 1 utas, 1 Loop Peristiwa, 1 mesin V8, dan libuv.


Jika kita memulai utas X, maka akan terlihat seperti ini:
1 proses Node.js, utas X, Loop Peristiwa X, mesin X V8, dan X libuv.


Secara skematis, tampilannya sebagai berikut


gambar


Mari kita ambil contoh.


gambar


Server web paling sederhana di Express. Ada 2 rute'a - / dan / operasi gemuk.


Ada juga fungsi generateRandomArr (). Dia mengisi array dengan dua juta catatan dan mengatasinya. Mari kita mulai server.


Kami membuat permintaan untuk / operasi lemak. Dan pada saat itu ketika operasi pengurutan array dilakukan, kami mengirim permintaan lain ke rute /, tetapi untuk mendapatkan jawaban kita harus menunggu sampai array diurutkan. Ini adalah implementasi utas tunggal klasik. Sekarang kita hubungkan modul worker_threads.


gambar


Kami membuat permintaan ke / operasi-gemuk dan kemudian - ke /, dari mana kami segera mendapatkan jawabannya - Halo dunia!


Untuk operasi pengurutan array, kami mengangkat utas terpisah yang memiliki instance Peristiwa Perulangan, dan itu tidak memengaruhi eksekusi kode pada utas utama.


Sebuah thread akan "dihancurkan" ketika tidak memiliki operasi untuk dilakukan.


Kami melihat kode sumbernya. Kami mendaftarkan pekerja di saluran 26 dan, jika perlu, meneruskan data ke sana. Dalam hal ini, saya tidak mentransmisikan apa pun. Dan kemudian kita berlangganan acara: kesalahan dan pesan. Pada pekerja, fungsinya disebut, array dua juta catatan diurutkan. Segera setelah diurutkan, kami mengirim hasilnya ke aliran utama ok melalui post_message.


gambar


Di utas utama, kami menangkap pesan ini dan mengirimkan hasilnya untuk menyelesaikan. Pekerja dan utas utama memiliki memori yang sama, jadi kami memiliki akses ke variabel global dari keseluruhan proses. Ketika kami mentransfer data dari arus utama ke pekerja, pekerja hanya mendapatkan salinan.


Kita dapat menggambarkan aliran utama dan aliran pekerja dalam satu file. Modul worker_threads menyediakan API yang dapat digunakan untuk menentukan di mana utas kode saat ini sedang dijalankan.


gambar


Informasi tambahan


Saya membagikan tautan ke sumber daya yang bermanfaat dan tautan ke presentasi Ryan Dahl ketika ia mempresentasikan Peristiwa Peristiwa (menarik untuk dilihat).


Perulangan acara


  1. Terjemahan artikel dari dokumentasi Node.js
  2. https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
  3. https://habr.com/en/post/336498/

Worker_threads


  1. https://nodejs.org/api/worker_threads.html#worker_threads_worker_workerdata - API
  2. https://habr.com/ru/company/ruvds/blog/415659/
  3. https://nodesource.com/blog/worker-threads-nodejs/
  4. Slide asli dari presentasi Ryan Dahl (melalui VPN)

Source: https://habr.com/ru/post/id479062/


All Articles