Mengenal Asynchronous lebih dalam

karena asyncrhonous tidak sebatas semantik

Mungkin sudah banyak tulisan yang membahas tentang Sync/Asynchronous, benefitnya, cara menggunakannya menggunakan salah satu bahasa program runtime, dsb.

Karena akhir-akhir ini menulis kode yang "closer to metal" dan berurusan dengan async i/o, jadi gue mau berbagi sedikit tentang apa yang gue pelajari.

Di tulisan ini, kita akan membahas tentang sedikit "under the hood" asynchornous yang biasanya berkaitan dengan operasi I/O, yang mana bersifat blocking.

I/O what?

Arti I/O sebenarnya sangat simple, dan pasti sudah tau kalau I/O adalah tentang input dan output. Kita gak jauh-jauh dulu bahas I/O yang kompleks, mari kita ambil contoh paling sederhana tentang I/O:

$ cat << EOF > io.py

nama = input("siapa namamu?")

print(nama)

print("hai!")

EOF

$ python3 io.py

siapa namamu?fariz

fariz

hai!

Lihat bagaimana print("hai!") hanya akan dipanggil setelah program berhasil menerima input dari pengguna? Silahkan ganti kasus menjadi mengambil data dari database, membuka berkas, dsb yang mana lebih kompleks dan praktikal.

Async I/O what?

Oke move on. Sekarang kita ingin menampilkan berkas yang ada di file system. Disini kita akan pakai Node.js, karena.. Harusnya sudah tau jawabannya. Nanti akan kita bahas lebih detailnya setelah ini.

$ echo "surprise!" >> open_for_surprise.txt

$ cat << EOF > io.js

const fs = require('fs')
const content = fs.readFileSync('./open_for_surprise.txt')

console.log(content.toString())
console.log('hai!')

EOF

$ node io.js

surprise!

hai!

Contoh diatas adalah blocking, baris console.log('hai') harus menunggu baris readFileSync dieksekusi! Sekarang mari kita buat versi async nya.

$ cat << EOF > aio.js

const fs = require('fs')
const content = fs.readFile('./open_for_surprise.txt', (err, data) => {
  console.log(data.toString())
})

console.log('hai!')

EOF

$ node aio.js

hai!
surprise!

Lihat bagaimana console.log('hai!') ter-eksekusi tanpa harus menunggu proses readFile selesai dieksekusi? Pola diatas menggunakan teknik "callback", tapi disini kita akan berdebat tentang teknik.

Node.js

Node.js adalah runtime untuk menjalankan kode JavaScript di server. Ya, Node.js bukanlah bahasa program. Lalu, apa yang membuat spesial dari Node.js JavaScript dibanding dengan bahasa program lain yang berjalan di server?

Async I/O is a first-class citizen!

Jika Ruby membutuhkan EventMachine,  Python membutuhkan Twisted, di JavaScript membutuhkan... Nothing, battery included. Dan sudah berada di standard/spesifikasi untuk bagian Web Application API mengingat saat ini kode yang dieksekusi di browser adalah JavaScript.

Tentunya bukan hanya Node.js, JavaScript runtime untuk menjalankan kode JavaScript di server. Namun karena ini yang paling populer dan stabil, kita akan ambil contoh dari Node.js saja.

Pastinya kita sudah mengetahui tentang Event Loop, secret sauce nya Node.js untuk mencapai Async I/O di JavaScript. Sebelum kita bahas itu, mari kita bicarakan hal yang sedikit lower-level terlebih dahulu.

Thread

Ya, thread adalah utas. Thanks Twitter.

Oke oke mari kita ambil contoh dari utas yang ada di Twitter. Kamu sedang membaca utas enggak penting menarik yang ada di Twitter, mungkin sekitar 50 tweet. Di tweet 23 kamu merasa capek, ingin minum terlebih dahulu, lalu baru lanjut baca lagi.

Jika ingin melanjutkan, tinggal buka URL dari utas tersebut, maka otomatis tersambung dengan twit sebelum & sesudahnya.

Pertanyaan nya, ingat ketika Twitter belum memiliki fitur utas?

LOL REMEMBER TRIOMACAN2000 ?

Ketika Twitter belum menggunakan "fitur" thread, terlihat sulit untuk menentukan context. Beberapa pengguna ada yang kreatif menggunakan penomoran agar kita (pembaca) tetap berada pada konteks, and that's exactly what we are talking about.

Node.js adalah single-threaded by default, alasannya sederhana (menurut gue), agar pengguna (developer) tidak mendapatkan kesulitan (di awal) untuk menggunakan Node.js.

Thread singkatnya adalah konteks dari eksekusi sedangkan proses adalah kumpulan sumber daya yang berhubungan dengan komputasi. Proses bisa saja memiliki 1 thread atau lebih. Jika ada thread di Twitter tentang "fakboi twitter", maka itulah konteks dari eksekusi tersebut (bercerita tentang fakboi twitter dari A-Z).

Oke oke mungkin sedikit kabur ya. Kita lebih praktikal deh. Mari kita kembali ke 2009. Misal, kita ingin hack akun email mantan. Kita kebetulan nemu password nya di somewhere else yang sayangnya di hash menggunakan MD5.

MD5 bukanlah hasil dari proses enkripsi, melainkan dari proses hashing. Yang perlu kita lakukan hanyalah mencocokkan berbagai kata (plain text) yang di MD5 dengan md5 yang kita miliki alias bruteforce.

Misal dengan program (dan hardware) yang gue punya, gue bisa melakukan pengecekan 5000 hash/detik. Kalau single-threaded. Jika pakai 2 thread? Mungkin bisa menjadi 10000 hash/detik atau proses "pencocokkan" nya menjadi lebih cepat.

Ya, proses tersebut bisa saja kita buat single-thread ataupun multiple, tergantung si program (dan pemrogram). Sengaja bahas tentang thread disini karena berhubungan dengan Async I/O juga, dan agar lebih memberikan gambaran tentang asynchronous.

Kita tidak akan membahas tentang Paralel & Konkurensi disini, silahkan googling tentang itu dan buka link yang mengarah ke situs medium dari hasil googling kamu tersebut :))

Event Loop

Akhirnya kita membahas ke topik utama. Apa itu Event Loop? Tentu saya tidak akan bahas disini, silahkan googling aja.

Oke, event loop merupakan pendekatan untuk melakukan concurrency, yang mana menggunakan teknik polling. Jika kita bayangkan, jika sinkronous berarti menunggu proses tersebut selesai ("Tunggu, gue lagi query ke database") kalau asingkronous (menggunakan polling) berarti seperti ini:

  • "Udah kelar belum?", belum.
  • "Gimana dah kelar kan sekarang?", belum jugaaaa.
  • "Pasti sekarang dah kelar kan?", bac...
  • "Gimana?", iyaa, nih data yang lo butuhin.

Polling tersebut dilakukan oleh "scheduler", bisa native (1:1), green thread (N:1), atau hybrid (M:N).

Contoh dari native ini adalah epoll (UNIX) dan kqueue (BSD). Untuk green, itu seperti Node.js (meskipun gak 100% setuju karena Node.js single-threaded) dan hybrid itu seperti Goroutine.

Segala sesuatu pasti ada kekurangan dan kelebihannya, dan tidak akan kita bahas disini.

Untuk penggunaan native thread (misal epoll) tentu kita bisa menulisnya sendiri (misal dengan #include <sys/epoll.h> atau the npm way). Atau bisa menggunakan library yang "meng-abstrak" scheduler tersebut (seperti Tokio bila di Rust).

Berbicara tentang Node.js, mengambil dari nodejs.org, beginilah event loop yang terjadi di Node.js (user-level/N:1):

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Gak usah dijelaskan satu-satu, disana udah lengkap dan jelas :))

Maksud dari tulisan ini

Begini, ini tentang mental model. Datang dari Node.js, menulis kode Go & Rust, lalu mengatur "concurrency" bukanlah hal yang mudah di level konsep.

Di JavaScript Node.js, main thread nya adalah Event Loop dan untuk melakukan "multi-threading" kita membuat thread pool. Alias, gue enggak tau berapa thread yang dibuat, at least gue udah minta sekian.

Di Go, "multi-threading" di-handle "out of the box" oleh Goruntime dengan membuat Goroutines. Alias, gue minta sekian thread untuk proses ini, Go yang ngehandle, yang OS tau, si Go pakai banyak thread tapi enggak spesifik untuk proses yang gue maksud.

Di Rust, sebelumnya mendukung green thread tapi di rollback karena alasan "zero cost abstraction". Zero cost abstraction sederhananya adalah kamu tidak membayar untuk sesuatu yang tidak kamu gunakan. Solusinya, yakni dipindahkan ke level library (Mio misalnya).

Untuk bisa melakukan pendekatan Async di Rust, bisa menggunakan futures. Atau yang lebih higher-level nya menggunakan Tokio. Tokio ini runtime untuk melakukan asynchronous, namun menggunakan native thread.

Tulisan ini untuk web developer

JavaScript runtime di browser beragam. Untuk keluarga chromium menggunakan V8, untuk keluarga mozilla menggunakan SpiderMonkey, dan JSCore untuk Apple.

3 aja nih?

Oke lanjut, kita fokus di thread untuk mengeksekusi JavaScript aja. Seperti yang kita tau, di Node.js, untuk melakukan pendekatan multi-threading, kita harus membuat thread pool, kumpulan worker thread.

Begitupula di browser, enggak tau tapi kalau Servo udah stabil.

Sebagaimana yang biasa kita lakukan di Node.js juga, kita terbiasa membuat proses yang menggunakan 1 thread. Karena sederhana, berkomunikasi antar "thread" bukanlah sesuatu yang sangat mudah.

Jika melihat kondisi sekarang, kita menggunakan JavaScript bukan hanya untuk membuat situs interaktif, melainkan, sampai "merekonstruksi" bagaimana browser bekerja.

Lihat Virtual DOM yang mana "melakukan caranya sendiri" untuk mengatur DOM. Atau, penggunaan API yang paling low-level di browser: Canvas yang biasanya membutuhkan resources yang lumayan.

Jika semua proses berada di main thread, tidak menutup kemungkinan akan terjadinya "glitch" karena sibuknya thread meng-handle banyak pekerjaan.

Maka dari itu browser memiliki API seperti Web Worker, yang berguna untuk melakukan "multi-threading" di browser. Jika aplikasi yang kamu buat adalah misalnya menganalisa gambar di client-side, mungkin Web Worker tersebut berguna untuk kamu.

Jika hanya melakukan CRUD biasa, sepertinya web worker juga cocok untuk kamu karena why not.

Gue rasa penggunaan web worker enggak bikin rugi. Seperti, emangnya proses pengubahan USD to IDR "murah" di level client-side? Atau lokalisasi di level client-side. Atau, parsing tanggal di client-side.

Tentunya ini bukan silver bullet, cuma keresahan aja.

Penutup

Berdasarkan hasil studi banding gue ke berbagai bahasa program ternyata banyak mendapatkan insight. Salah satunya, sesederhana melakukan "asynchronous process" di JavaScript tapi ternyata ribet juga anjing kalau di Rust & Go.

Tapi hidup akan selalu berurusan dengan abstraksi. Dan sayangnya, tidak menutup kemungkinan kita akan berada di level abstraksi yang berbeda-beda, entah di atas atau lebih bawah.

Hidup selalu berurusan dengan konkurensi dan paralel, bagaimanapun, se-denial kita untuk tidak memikirkan itu, disuatu kondisi kita akan dipaksa untuk memikirkan itu.

Jika bukan untuk dipahami, setidaknya untuk diterapkan. Seperti pertanyaan "Untuk apa kita hidup?", mungkin kita tidak perlu memikirkan, cukup dijalankan saja. Sekarang gue tanya lagi:

"Untuk apa kita hidup?"

"Mengapa kita hidup?"

"Apakah hidup adalah tentang dimulai dengan bangun, dihabiskan dengan bekerja dan diakhiri dengan tidur?".

"Untuk apa kita menggunakan asynchronous model?"

"Apakah asynchronous sesederhana menggunakan async/await, Promise, atau callback?"

Ngapain dipikirin, yang penting digaji!

Ngapain dipikirin, yang penting masih nafas!

Lalu kita lupa bahwa ada senior yang sering ngomel fundamental, dan malaikat yang mencatat tingkah laku.

Sampai kita sadar, bahwa kehidupan tidak berjalan se-enak kita.

Ada kitab suci & spesifikasi.

Dan ada "Entitas" yang menilai.

Tenang, semua ada masa nya.

Bahkan, apakah kamu pernah memikirkan kenapa kamu membaca ini dan sampai baris ini?

Menikmati tulisan ini?

Blog ini tidak menampilkan iklan, yang berarti blog ini didanai oleh pembaca seperti kamu. Gabung bersama yang telah membantu blog ini agar terus bisa mencakup tulisan yang lebih berkualitas dan bermanfaat!

Pendukung

Kamu?