CPU dengan banyak core tidak membuat kode aplikasi menjadi paralel dengan sendirinya. Sistem operasi memang dapat menjadwalkan beberapa thread pada saat yang sama, tetapi bahasa dan runtime menentukan bagaimana pekerjaan aplikasi mencapai thread tersebut. Perbedaan ini menjelaskan mengapa Go, Rust, C++, Java, JavaScript, dan PHP sama-sama dapat memakai mesin multicore meskipun model pemrogramannya sangat berbeda.
Pertanyaan yang lebih berguna bukan sekadar apakah sebuah bahasa “multithreaded”. Yang perlu dilihat adalah bagaimana satu unit pekerjaan aplikasi berubah menjadi sesuatu yang dapat dijadwalkan oleh sistem operasi.
Native thread memetakan pekerjaan langsung ke OS thread
C++ dan Rust menyediakan native thread melalui standard library. Sebuah std::thread baru di C++ atau std::thread::spawn di Rust membuat thread eksekusi terpisah yang ditopang fasilitas threading platform.
Di C++:
#include <thread>
void work() {
// CPU-bound work
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
}Di Rust:
use std::thread;
fn main() {
let a = thread::spawn(|| {
// CPU-bound work
});
let b = thread::spawn(|| {
// CPU-bound work
});
a.join().unwrap();
b.join().unwrap();
}Jika ada dua thread runnable dan mesin memiliki setidaknya dua core CPU yang dapat dijadwalkan, sistem operasi dapat menjalankannya secara paralel. Bahasa tidak memerlukan user-space scheduler di antara application thread tersebut dan kernel scheduler.
Kontrol langsung ini memberi fleksibilitas tinggi, tetapi OS thread tidak gratis. Setiap thread membutuhkan bookkeeping kernel, stack space, scheduling state, serta sinkronisasi ketika data digunakan bersama. Desain yang membuat satu native thread untuk setiap pekerjaan kecil dapat menghabiskan banyak resource hanya untuk mengelola thread.
Go menempatkan runtime scheduler di antara goroutine dan thread
Go mengekspos goroutine, bukan meminta kode aplikasi membuat satu OS thread untuk setiap fungsi concurrent.
go taskA()
go taskB()Dua goroutine tersebut adalah unit pekerjaan pada level aplikasi. Go runtime menjadwalkan goroutine yang runnable ke sekumpulan OS thread yang jumlahnya lebih kecil atau dapat berubah, lalu sistem operasi menjadwalkan thread tersebut ke CPU core.
Hubungannya menjadi:
goroutines
|
Go scheduler
|
OS threads
|
CPU coresIni penting karena satu program dapat memiliki jauh lebih banyak goroutine daripada jumlah CPU core. Ribuan goroutine dapat menunggu network I/O, timer, channel, atau lock, sementara hanya bagian yang runnable yang membutuhkan waktu eksekusi CPU.
Karena itu, proses Go bukan “single-threaded karena goroutine bukan thread”. Runtime dapat memakai beberapa OS thread, dan beberapa goroutine dapat berjalan paralel ketika scheduler memiliki execution resource yang cukup.
Sebaliknya juga berlaku: menjalankan program Go pada mesin 16-core tidak otomatis membagi satu fungsi sequential ke 16 core. Parallelism baru muncul ketika memang ada pekerjaan runnable yang independen.
Java memiliki platform thread dan virtual thread
Java sejak lama menyediakan platform thread melalui java.lang.Thread. Platform thread berhubungan dengan operating-system thread ketika menjalankan kode Java.
Java juga menyediakan virtual thread. Dari sudut pandang aplikasi, virtual thread tetap merupakan Thread, tetapi tidak terikat secara permanen ke satu OS thread. Java runtime dapat menangguhkan virtual thread yang sedang blocking dan memberikan underlying carrier thread kepada virtual thread lain.
Model ini berguna untuk aplikasi dengan sangat banyak task concurrent yang menghabiskan banyak waktu menunggu I/O. Namun, virtual thread tidak otomatis membuat perhitungan CPU-intensive menjadi lebih cepat.
Untuk pekerjaan CPU-bound, batas utamanya tetap kapasitas eksekusi CPU. Jika sebuah mesin hanya mampu menjalankan delapan thread CPU-heavy secara bersamaan, membuat satu juta virtual thread tidak menciptakan satu juta core.
Di sinilah perbedaan concurrency dan parallelism menjadi penting:
concurrency = banyak task dapat membuat progress sepanjang waktu
parallelism = beberapa task benar-benar dieksekusi pada saat yang samaRuntime dapat membuat concurrency menjadi murah tanpa menambah kapasitas parallel CPU secara fisik.
JavaScript mempertahankan main execution model sederhana tetapi dapat memakai worker
JavaScript di browser dan Node.js sering disebut single-threaded karena callback JavaScript biasa dieksekusi pada main event-loop thread.
Pernyataan itu tidak lengkap jika diartikan sebagai “JavaScript hanya bisa memakai satu CPU core.”
Node.js dapat membuat Worker Threads untuk pekerjaan JavaScript yang CPU-intensive. Browser menyediakan Web Workers untuk kebutuhan yang serupa. Setiap worker memiliki JavaScript execution context sendiri dan dapat berjalan independen dari main thread.
Secara konseptual, program Node.js dapat terlihat seperti:
main event-loop thread
|
+-- worker 1
+-- worker 2
+-- worker 3Event loop sangat efektif untuk aplikasi I/O-heavy karena main thread tidak membutuhkan satu blocking application thread untuk setiap operasi socket. Pekerjaan JavaScript yang CPU-heavy berbeda. Komputasi panjang di main thread akan menahan callback JavaScript lain sampai pekerjaan itu selesai atau melepaskan eksekusi, sehingga worker atau proses tambahan menjadi relevan ketika dibutuhkan parallelism CPU yang nyata.
“Single-threaded main loop” dan “single-core application” bukan hal yang sama.
PHP umumnya menggunakan banyak proses untuk menyebar pekerjaan ke core
Deployment web PHP tradisional sering memakai PHP-FPM. Model eksekusi yang umum bukan satu proses PHP permanen dengan banyak application thread. Sebaliknya, tersedia pool yang berisi beberapa worker process, dan worker yang berbeda dapat menangani request yang berbeda.
Pada server delapan core, bentuk sederhananya dapat seperti:
request A -> PHP-FPM worker process 1
request B -> PHP-FPM worker process 2
request C -> PHP-FPM worker process 3
request D -> PHP-FPM worker process 4Sistem operasi dapat menjadwalkan proses-proses tersebut ke CPU core yang berbeda. Artinya, banyak core CPU tetap berguna meskipun satu request PHP biasa mengeksekusi kode PHP secara sequential.
Batasannya terlihat ketika satu request berisi satu pekerjaan CPU-bound yang besar. Satu pekerjaan sequential tidak otomatis tersebar ke seluruh core hanya karena server memiliki banyak PHP-FPM worker. Untuk memakai lebih banyak core pada pekerjaan tersebut, tugas harus dibagi ke beberapa proses, specialized worker, extension, atau execution system lain.
Inilah alasan process-based concurrency dapat menskalakan web server dengan baik, tetapi tetap berperilaku berbeda dari in-process multithreading.
CPU multicore tetap berguna ketika satu request berjalan sequential
Anggapan bahwa request single-threaded membuat core CPU lain sia-sia mengabaikan concurrency pada level server.
Misalnya sebuah server memiliki 16 logical CPU dan menerima 100 HTTP request yang independen. Walaupun setiap request menjalankan kode sequential, server tetap dapat mengeksekusi beberapa request sekaligus ketika handler ditempatkan pada proses atau thread yang berbeda.
Yang menentukan adalah jumlah pekerjaan independen yang runnable:
one sequential task
-> limited parallelism
many independent tasks
-> scheduler can distribute them across cores
one task split into independent pieces
-> the pieces may execute in parallelServer PHP-FPM yang sibuk, Node.js cluster, server Java, dan service Go semuanya dapat membuat banyak core tetap terpakai. Perbedaannya ada pada struktur runtime yang dipakai untuk mencapai kondisi tersebut.
Model kompilasi dan model threading adalah dua hal terpisah
Apakah sebuah bahasa dikompilasi langsung menjadi native machine code tidak menentukan apakah bahasa tersebut dapat menjalankan pekerjaan secara paralel.
C++, Rust, dan Go lazim menghasilkan native executable, tetapi model concurrency ketiganya berbeda. Java biasanya menjalankan bytecode melalui JVM dan tetap dapat memakai banyak native thread serta CPU core. JavaScript berjalan melalui engine seperti V8 dan dapat memakai worker thread. PHP biasanya dieksekusi melalui runtime dan dapat memakai beberapa worker process.
Lapisan tersebut terpisah:
source language
|
compiler / VM / runtime
|
concurrency model
|
OS scheduler
|
CPU coresNative executable dapat tetap hampir seluruhnya single-threaded. Sebaliknya, aplikasi berbasis VM dapat membuat puluhan core sibuk. Format binary tidak menentukan concurrency topology.
Model yang tepat bergantung pada batas workload
Native thread cocok untuk workload yang membutuhkan eksekusi paralel eksplisit dan kontrol langsung terhadap shared memory. Lightweight runtime task seperti goroutine atau virtual thread membuat jumlah operasi concurrent yang besar lebih mudah dikelola. Event-loop system menghindari kebutuhan satu application thread untuk setiap I/O yang sedang menunggu. Multi-process system memisahkan worker dan membiarkan sistem operasi mendistribusikan request terpisah ke banyak core.
Batas praktisnya adalah apakah workload memang memiliki unit eksekusi yang independen.
Untuk server yang menangani banyak request terpisah, PHP berbasis proses dapat memakai CPU multicore dengan efektif. Untuk service Go dengan ribuan operasi network concurrent, goroutine memungkinkan runtime melakukan multiplexing pekerjaan ke beberapa thread. Untuk algoritma CPU-heavy di Rust atau C++, native thread atau thread pool dapat membagi komputasi secara langsung. Untuk JavaScript, worker diperlukan ketika pekerjaan CPU-heavy harus berjalan paralel dengan main event loop.
Hardware hanya menyediakan kapasitas eksekusi. Runtime model menentukan bagaimana pekerjaan aplikasi diekspos ke kapasitas tersebut, sedangkan arsitektur aplikasi menentukan apakah tersedia cukup pekerjaan independen untuk benar-benar memakainya.