Memahami Single Thread dan Non-Blocking Pada JavaScript

Memahami sifat single-threaded pada JavaScript sangat penting untuk menulis kode yang efisien dan mencegah bottleneck yang dapat membuat aplikasi menjadi tidak responsif.

Saat mempelajari bahasa pemrograman JavaScript, mungkin kita sudah pernah mendengar bahwa JavaScript itu adalah bahasa yang menggunakan model single thread.

Dalam ilmu komputer, thread eksekusi adalah urutan instruksi terkecil yang dapat dikelola secara mandiri oleh scheduler, yang biasanya merupakan bagian dari sistem operasi. — Wikipedia

Nah sebelum lebih jauh, mari mengenal dua tipe thread yang ada pada dunia komputer, yaitu thread user level (diatur oleh runtime) dan thread kernel level (diatur oleh sistem operasi). Single thread pada Javascript itu termasuk pada user level yang diatur oleh JavaScript runtime engine.

Apa Itu Single Thread pada Javascript ?

Single Thread pada Javascript adalah sebuah urutan operasi yang akan di eksekusi oleh JavaScript Engine satu per satu atau menjalankan satu tugas dalam satu waktu.

Selain itu, thread juga bertugas untuk mengelola heap atau alokasi memori pada JavaScript Engine.

JavaScript awalnya di desain untuk menangani interaktifitas pada halaman web di sekitar tahun 1995. Komputer pada saat itu mempunyai sumber daya yang terbatas dan tidak secepat komputer pada masa sekarang, oleh karena itu agar mencapai kesederhanaan dan efisiensi, JavaScript dibuat single-threaded.

Namun seiring berjalan nya waktu, JavaScript berkembang untuk bisa melakukan tugas-tugas yang lebih berat seperti sekarang ini.

Apakah tugas yang berat bisa memperlambat eksekusi tugas-tugas pada JavaScript?

Jawaban nya bisa iya dan bisa juga tidak.

Untuk menjawab pertanyaan diatas secara utuh, mari kita lihat dua jenis operasi yang ada di Javascript berikut ini.

Blocking

Operasi blocking akan menghentikan eksekusi kode selanjutnya hingga operasi tersebut selesai. Selama operasi blocking berlangsung, JavaScript tidak dapat melakukan tugas lain, yang dapat membuat aplikasi menjadi tidak responsif.

Contoh sederhana operasi blocking pada node.js dengan fs.readFileSync:

const fs = require('fs');

const data = fs.readFileSync('file_besar.json', 'utf-8'); // Operasi blocking
console.log(data); 

console.log("Kode ini akan dieksekusi setelah file selesai dibaca."); // Tertahan hingga readFileSync selesai

Jika kode diatas dijalankan, eksekusi akan tertahan pada fs.readFileSync dikarenakan operasi blocking yang mengharuskan untuk menunggu operasi selesai dan kemudian barulah eksekusi dilanjut dengan statement console.log yang ada dibawahnya.

Non-Blocking

Sedangkan operasi non-blocking, tidak menghentikan eksekusi kode selanjutnya. Tanpa perlu menunggu operasi selesai, JavaScript akan melanjutkan eksekusi kode lain dan akan diinformasikan ketika operasi non-blocking selesai. Hal ini memungkinkan aplikasi tetap responsif meskipun sedang memproses operasi yang lama.

Contoh operasi non-blocking pada node.js:

const fs = require('fs');

fs.readFile('file_besar.json', 'utf-8', (err, data) => {
  if (err) throw err;
  console.log(data); 
});

console.log("Kode ini akan dieksekusi langsung, tanpa menunggu file selesai dibaca.");

Saat kode diatas di jalankan, maka semua kode akan di eksekusi sekaligus. Namun return data operasi non-blocking pada fs.readFile akan di dapat setelah proses read file dinyatakan selesai.

Pemilihan operasi yang tepat atau balancing act biasanya dipengaruhi oleh beberapa hal, seperti:

  • Kebutuhan sistem

  • Performa yang di inginkan

  • Kompleksitas, seperti ketergantungan suatu operasi dengan operasi lain untuk mendapatkan hasil yang konsisten.

Namun secara umum developer akan menggunakan operasi blocking untuk tugas yang ringan atau cepat, kemudian menggunakan non blocking untuk tugas yang berat.

Mengenal Concurrency

Walaupun menggunakan model single thread, JavaScript tetap bisa menangani banyak tugas dalam satu waktu. Bahasa pemrograman JavaScript di design untuk mampu melakukan concurrency, yaitu kemampuan menerima banyak tugas dalam satu waktu.

Namun, tugas-tugas tersebut belum tentu dikerjakan dalam satu waktu juga. Tugas-tugas tersebut akan diatur se-efisien mungkin agar tidak perlu menunggu satu per satu tugas diselesaikan terlebih dahulu. Hal ini dapat terjadi dengan bantuan Event Loop dan Asynchronous programming.

Event Loop dan Asynchronous Programming bekerja sama untuk memastikan bahwa tugas-tugas yang berjalan lama tidak memblokir eksekusi kode lain, sehingga JavaScript tetap responsif. Hal ini menjadi kunci dalam memahami perbedaan antara operasi blocking dan non-blocking.

Lalu apakah Concurrency ini lebih mirip dengan sequence atau bisa berjalan paralel ya?

Saat membahas single thread tadi, JavaScript memang lebih terlihat seperti sistem yang sequence atau meng-eksekusi task secara berurutan, namun dengan adanya operasi non-blocking mengakibatkan JavaScript bisa mengerjakan tugas yang “berat“ tanpa menunda untuk mengerjakan tugas-tugas lainnya (concurrency).

Nah concurrency ini berbeda dengan serialisasi atau paralelisme. Bedanya seperti apa? berikut penjelasan nya:

  • Serialisasi atau sequence menggambarkan proses yang berurutan dan membutuhkan konsistensi, tetapi bisa menjadi tidak efisien untuk tugas-tugas yang kompleks atau banyak. Contoh analoginya: Seorang koki yang memasak hidangan satu per satu, koki tersebut hanya memasak satu hidangan sampai selesai, setelah itu baru memasak hidangan yang lain.

  • Paralelisme menunjukkan bagaimana multiple resources bisa digunakan untuk menyelesaikan tugas-tugas secara bersamaan, meningkatkan kecepatan tetapi memerlukan lebih banyak sumber daya. Contoh analoginya: Beberapa koki yang memasak hidangan secara bersamaan.

  • Concurrency mengilustrasikan bagaimana satu resource bisa menangani banyak tugas secara efisien dengan beralih antara tugas-tugas tersebut, mirip dengan multitasking dalam kehidupan sehari-hari. Contoh analoginya: Seorang koki yang memasak beberapa hidangan secara bergantian (memotong bahan satu hidangan, memanaskan wajan untuk hidangan lain, dst).

Ilustrasi Gambar diambil dari postingan medium

Tapi saat ini, Javascript juga sudah mendukung konsep paralelisme (multi-thread) dengan mengkombinasikan main thread dengan thread pendukung, seperti:

  • Thread worker pada Node.js

  • Web Worker pada web dan Deno

  • Workers API pada Bun

Kita bisa menggunakan thread pendukung tersebut saat perlu menangani tugas-tugas berat, seperti memproses file besar atau menjalankan operasi yang membutuhkan penggunaan CPU tinggi.

Namun, perlu diingat bahwa penggunaan thread yang lebih banyak juga memiliki trade-off, seperti kompleksitas kode yang meningkat dan potensi overhead dalam mengelola thread tersebut.

What Next?

Kita telah mempelajari bahwa JavaScript, meskipun pada dasarnya single-threaded, mampu menangani concurrency dengan mekanisme non-blocking. Bahkan, sekarang JavaScript telah berkembang untuk mendukung operasi multi-threading untuk kebutuhan yang lebih intensif.

Namun, di balik semua itu, tetap ada satu pertanyaan besar: bagaimana sebenarnya JavaScript mengatur eksekusi kode asynchronous dan menjaga agar aplikasi tetap responsif? Jawabannya terletak pada mekanisme yang dikenal sebagai Event Loop.

Kita akan coba mendalami Event Loop pada artikel selanjutnya.