Walaupun Node.js tidak mendukung multi-threading, ia memiliki implementasi event loop untuk mengerjakan kode program secara asynchronous. Sebenarnya kemampuan mengerjakan kode program secara asynchronous tidak ada kaitannya dengan threading, tetapi dalam banyak hal, saya tidak perlu tahu lebih detail. Selama kode program bisa dijalankan secara asynchronous (misalnya melalui Promise dan timer), saya tidak pernah harus mengetahui implementasi detail event loop di Node.js. Akan tetapi, saat saya mencoba menerapkan konsep worker (yang umum dipakai di bahasa multi-threading) dengan menggunakan Promise, saya mulai menemukan banyak masalah. Sebagai contoh, saya membuat kode program seperti berikut ini:
Hasilnya pada saat dijalankan akan terlihat seperti berikut ini:
Perbandingan Dengan Threading Di Java
Pada bahasa yang mendukung multi-threading seperti Java, kode program untuk worker1 dan worker2 akan bekerja di thread masing-masing.
Sebagai contoh, berikut ini adalah versi Java-nya:
Hasil eksekusi program di atas akan terlihat seperti:
Keduanya terlihat sama, bukan? Node.js dapat mencapai hal yang sama dengan mengerjakan kode program di setInterval() dan Promise secara silih berganti di dalam event loop. Setiap detik, worker1 dan worker2 tetap akan mengirim pesan ke PubSub. Walaupun demikian, kode program Node.js saya ternyata memiliki banyak masalah tersembunyi! Masalah ini muncul karena mereka sebenarnya dikerjakan silih berganti, dan bukan secara paralel seperti seharusnya di bahasa multi-threaded.
Sebagai contoh, apa yang terjadi bila worker1 mengalami kendala, misalnya bekerja terlalu lambat? Pada versi multi-threaded di Java,
thread worker2 akan tetap berjalan seperti biasanya dan tidak dipengaruhi oleh worker1 sama sekali. Untuk membuktikannya, saya menambahkan
infinite loop di worker1 sehingga kode program Java saya terlihat seperti:
Hasil eksekusi program Java akan terlihat seperti:
Tidak ada keluaran dari worker1 lagi selain baris kedua. Hal ini karena worker1 tertunda oleh while (true); yang tidak akan pernah selesai dikerjakan. Walaupun demikian, worker2 tidak terpengaruh dan tetap mengirim pesan ke PubSub setiap detik-nya.
Bagaimana dengan versi Node.js? Untuk mencobanya, saya mengubah kode program JavaScript saya menjadi seperti:
Hasilnya terlihat seperti berikut ini:
Setelah worker1 terblokir oleh while (true);, seluruh proses akan tertunda, termasuk worker2. Hal ini yang disebut sebagai “memblokir event loop”. Kinerja aplikasi Node.js tersebut secara keseluruhan akan terkena dampaknya walaupun hanya satu operasi asynchronous yang lambat.
Event Loop Yang Terblokir
Pada dunia nyata, jeda bukanlah sesuatu yang bisa dihindari, terutama bila berhadapan dengan I/O. Agar lebih realistis, saya akan mengubah while (true); menjadi sebuah looping yang lama seperti for (let a = 0; a < 9999999999; a++);. Ketika menjalankan program kembali, saya akan memperoleh hasil seperti:
Terlihat bahwa jeda pada salah satu bagian dari operasi asynchronous menyebabkan kode program di setInterval tidak lagi dijalankan setiap detik sebagaimana seharusnya. Baik worker1 dan worker2 mengalami jeda yang cukup lama, walaupun sebenarnya yang sibuk hanya worker1. Ini adalah salah satu alasan mengapa selalu dokumentasi Node.js menyarankan untuk selalu berusaha membuat kode program asynchronous yang kecil dan singkat. Semakin lama dan semakin kompleks sebuah Promise atau timer, semakin besar kemungkinan operasi tersebut akan mem-blokir event loop. Saya bisa menggunakan Clinic.js untuk memeriksa kesehatan event loop, dengan memberikan perintah clinic doctor. Untuk kode program di atas, saya akan mendapatkan hasil seperti berikut ini:
Pada grafis yang dihasilkan perintah clinic doctor di atas, terlihat bahwa event loop terjeda sampai lebih dari 30 detik. Ini adalah jeda
yang sangat tinggi, bila dibandingkan dengan versi awal (normal) seperti yang terlihat pada gambar berikut ini:
Pada grafis di atas, jeda paling tinggi untuk event loop hanya sekitar 0.4 milidetik.
Solusi
Untuk menghindari event loop yang terblokir, saya bisa mem-partisi kode program yang lambat menjadi beberapa bagian kecil dan menggunakan setImmediate() untuk memberikan kesempatan agar operasi lain di event loop dikerjakan terlebih dahulu. Sebagai contoh, saya akan mengubah kode program Node.js saya menjadi seperti berikut ini:
Bila kode program di atas dijalankan, hasilnya akan terlihat seperti:
Kali ini worker1 dan worker2 terlihat lebih responsif. Walaupun demikian, mereka tetap tidak dikerjakan secara teratur
setiap detik. Selain itu, semakin banyak iterasi jangka panjang yang tertunda hanya akan membuat mereka semakin lama seiring waktu
berlalu. Tanpa multi-threading yang bisa menjalankan beberapa kode program secara paralel dan bersamaan, ini adalah hasil paling
maksimum yang bisa saya capai.
Studi Kasus Pada PubSub
Pada contoh di atas, dampak dari ter-blokir-nya event loop terasa sangat jelas. Akan tetapi pada kondisi tertentu, saya bisa saja
menjumpai kasus aneh yang tidak saya duga penyebabnya adalah event loop. Sebagai contoh, library PubSub untuk Node.js secara bawaan
akan mengaktifkan batching. Saya akan mengubah function publishEvent() di kode program saya untuk mengirim ke PubSub
paling lambat 100 milidetik setelah permintaan pengiriman diterima, seperti pada contoh berikut ini:
Terlihat sederhana, bukan? Saya ingin pesan dikirim ke PubSub secepat mungkin tanpa jeda, sepertinya batas waktu 100 milidetik
seharusnya tidak masalah, bukan? Ternyata tidak sesederhana itu! Saat event loop terlalu sibuk, saya akan memperoleh hasil
seperti pada berikut ini:
Pada hasil eksekusi di atas, terlihat dengan jelas terdapat jarak yang jauh antara baris “Mengirim pesan ke PubSub” dan “Pesan berhasil dikirim ke PubSub”. Rentang waktu yang ada mencapai 6 detik. Mengapa demikian? Bukankah tidak ada proses yang berat di function publishEvent()? Hanya satu baris untuk mengirim pesan ke PubSub! Perlu diingat bahwa library PubSub tidak mengirim pesan secara synchronous, melainkan asynchronous melalui MessageQueue yang mengimplementasikan EventEmitter. Dengan demikian, bila event loop terblokir, maka proses pengiriman pesan ke PubSub juga terkena dampaknya. Batching yang seharusnya hanya perlu menunggu 100 milidetik, menjadi harus menunggu hingga operasi yang mem-blokir event loop selesai dikerjakan.
Multi Threading Di Node.js
Solusi lain untuk mengatasi pemblokiran event loop adalah dengan menggunakan worker thread. Thread? Iya, benar, Node.js 10.5.0 ke atas sudah dilengkapi dengan kemampuan membuat thread baru yang disebut sebagai worker thread. Sebagai contoh, saya akan mengubah kode program Node.js saya menjadi seperti berikut ini:
Hasil eksekusinya akan terlihat seperti:
Kali ini worker2 tetap bekerja walaupun worker1 sedang sibuk bekerja hampir setiap detik. Juga tidak ada masalah keterlambatan
lagi dalam pengiriman pesan ke PubSub untuk worker2. Tentu saja worker1 tetap lambat karena saya sengaja menambahkan
perulangan yang lama. Iterasi ke dua-nya baru muncul pada detik ke-7 (dimana worker2 sudah mencapai iterasi ke-8).
Walaupun worker thread membuat thread baru, pengalaman memakainya memiliki rasa yang sangat berbeda dari model
pemograman multi-threaded di bahasa seperti Java. Saya juga tetap harus berhati-hati agar tidak memblokir event loop
di Node.js walaupun sudah ada worker thread. Oleh sebab itu, secara garis besar, Node.js tetap masuk dalam
kategori single-threaded.