Dua chapter sebelumnya berulang kali menyebut “executor” sebagai pihak yang meng-poll future, tetapi executor itu sendiri masih berupa kotak hitam yang kita pinjam lewat Runtime::new(). Saatnya kotak itu dibuka: runtime tokio adalah paket lengkap berisi scheduler multi-thread, timer, dan driver I/O yang mengubah future malas menjadi pekerjaan nyata. Chapter ini membedah isi runtime, memperkenalkan makro #[tokio::main] sebagai cara standar memakainya, membuktikan bahwa makro tersebut hanyalah penulis otomatis pola block_on yang sudah kita kenal, dan melihat bagaimana scheduler membagi tugas ke thread worker.
Tokio dan Fitur-fiturnya #
Tokio bukan bagian dari pustaka standar, dan desainnya memungkinkan pengguna mengaktifkan hanya komponen yang dibutuhkan lewat feature flags pada cargo. Pilihan fiturnya memengaruhi ukuran binary dan waktu kompilasi, jadi layak dihafal ringkas:
| Feature | Isi yang diaktifkan |
|---|---|
rt | inti runtime dan block_on |
rt-multi-thread | scheduler multi-thread dengan thread worker |
macros | makro #[tokio::main], join!, dan select! |
time | timer sleep, timeout |
fs | operasi berkas tokio::fs |
net | jaringan tokio::net (TcpListener, TcpStream) |
io-util | trait AsyncReadExt dan AsyncWriteExt |
sync | primitif sinkronisasi async (misalnya channel) |
Untuk proses belajar kita memakai features = ["full"] yang menyalakan semuanya sekaligus, seperti konfigurasi di Cargo.toml Chapter async fn. Setelah aplikasi matang, memilih fitur secara minimal — misalnya hanya rt-multi-thread, macros, dan time untuk program yang tidak menyentuh berkas — membuat kompilasi lebih ringan. Berikut isi Cargo.toml sebagai pengingat:
[dependencies]
tokio = { version = "1", features = ["full"] }Menjalankan Runtime dengan #[tokio::main] #
Cara paling lazim memakai tokio adalah menandai main dengan atribut #[tokio::main]. Atribut ini mengizinkan main ditulis sebagai async fn — kebalikan dari galat E0752 di Chapter async fn — sekaligus membangun runtime di belakang layar:
use std::time::{Duration, Instant};
async fn download_data(name: &str, seconds: u64) -> String {
tokio::time::sleep(Duration::from_secs(seconds)).await;
format!("{} selesai", name)
}
#[tokio::main]
async fn main() {
println!("berjalan di thread {:?}", std::thread::current().name());
let start = Instant::now();
let prices = download_data("daftar harga", 1).await;
let stock = download_data("daftar stok", 2).await;
println!("{}", prices);
println!("{}", stock);
println!("total: {} detik", start.elapsed().as_secs());
}Output yang Dihasilkan:
berjalan di thread Some("main")
daftar harga selesai
daftar stok selesai
total: 3 detikMari kita bedah kode di atas:
- Baris 8 (
#[tokio::main]): atribut ini memicu ekspansi makro sebelum kompilasi — tubuhasync fn maindibungkus menjadi runtime +block_on, persis pola manual yang kita tulis sendiri di chapter-chapter sebelumnya. - Baris 10 (
println!("berjalan di thread {:?}", ...)):std::thread::current().name()menanyakan identitas thread yang sedang menjalankan kode. HasilnyaSome("main")— future teratas dariblock_ondijalankan di thread pemanggilnya, bukan di thread worker. - Baris 13-14 (
download_data(...).awaitdua kali): dua unduhan berdurasi 1 dan 2 detik ditunggu berurutan, sehingga totalnya 3 detik. Runtime sudah aktif, tetapi.awaityang berurutan tetap membuat penantinan berbaris seri — mesinnya berbeda dari Chapter Mengapa Async, hasil waktunya sama. - Baris 17 (
println!("total: {} detik", ...)): pengukuran menutup bukti bahwa#[tokio::main]bekerja identik dengan pola manual.
Apa yang Sebenarnya Dilakukan Atribut Itu #
Atribut #[tokio::main] bukan sihir bahasa baru; atribut adalah makro prosedural yang menulis ulang kode sebelum kompilator melihatnya. Bentuk hasil tulisannya kira-kira seperti program berikut, yang kita jalankan secara eksplisit:
use std::time::{Duration, Instant};
async fn download_data(name: &str, seconds: u64) -> String {
tokio::time::sleep(Duration::from_secs(seconds)).await;
format!("{} selesai", name)
}
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let start = Instant::now();
let prices = download_data("daftar harga", 1).await;
let stock = download_data("daftar stok", 2).await;
println!("{}", prices);
println!("{}", stock);
println!("total: {} detik", start.elapsed().as_secs());
});
}Output yang Dihasilkan:
daftar harga selesai
daftar stok selesai
total: 3 detikMari kita bedah perbedaannya terhadap versi beratribut:
- Baris 9 (
let runtime = tokio::runtime::Runtime::new().unwrap();):Runtime::new()membangun runtime multi-thread lengkap: scheduler beserta thread worker sejumlah core CPU, timer, dan driver I/O semuanya siap dipakai. Inilah pekerjaan yang disembunyikan#[tokio::main]. - Baris 11-18 (
runtime.block_on(async { ... })): blokasyncmenggantikan tubuhasync fn main, danblock_onmenggerakkannya sampai selesai di threadmain. Output kedua program identik — bukti makro hanyalah penulis otomatis pola ini. - Variabel
runtimehidup sampai akhirmain: selama variabel tersebut belum keluar scope, scheduler tetap hidup. Aturan kepemilikan dari Section Ownership berlaku seperti biasa: dropruntimeberarti membubarkan seluruh mesin penjadwalan.
Runtime juga bisa dibatasi ke satu thread lewat #[tokio::main(flavor = "current_thread")] — berguna untuk program kecil atau lingkungan yang tidak boleh membuat thread tambahan; nama flavor-nya setara Builder::new_current_thread. Kita cukup tahu kedua rasa itu ada; multi-thread tetap bawaan yang tepat untuk belajar.
Scheduler Multi-Thread di Balik Layar #
Apa yang dibeli Runtime::new() dengan segala thread worker-nya? Pertama, work stealing: setiap worker memegang antrian task lokal, dan worker yang kehabisan pekerjaan boleh mencuri task dari antrian worker lain, sehingga beban tersebar otomatis tanpa kita mengatur apa pun. Kedua, jumlah worker menempel pada jumlah core CPU — dua core berarti dua worker, delapan core berarti delapan. Ketiga, task boleh berpindah thread di antara titik .await, sehingga task yang berhenti di worker 1 bisa dilanjutkan di worker 2 tanpa kode kita menyadarinya.
Selain scheduler, runtime menjalankan dua mesin pendukung yang bekerja diam-diam. Pertama, timer: setiap tokio::time::sleep mendaftarkan tenggat waktunya ke pengatur timer milik runtime, dan ketika waktu itu tiba, runtime membangunkan future yang bersangkutan agar di-poll ulang. Kedua, driver I/O: future jaringan mendaftarkan socket-nya ke driver yang memantau kesiapan baca/tulis lewat mekanisme event milik sistem operasi, sehingga future hanya dijadwalkan ketika socket-nya benar-benar siap. Kedua mesin inilah alasan sleep di Chapter .await dan TcpStream di Chapter I/O Async bisa bangun kembali tanpa ada satu thread pun yang berdiri menjaga penantian mereka.
Pada program di atas seluruh pekerjaan masih terjadi di thread main karena kita belum meluncurkan satu task pun — future teratas block_on memang selalu berjalan di thread pemanggilnya. Bagaimana task justru menyeberang ke thread worker bernama tokio-rt-worker akan terbukti lewat tokio::spawn di chapter berikutnya.
Ringkasan #
Runtime tokio adalah executor sungguhan yang menunggangi trait Future dari pustaka standar: scheduler multi-thread dengan work stealing, timer, dan driver I/O yang dinyalakan sesuai feature flags pada Cargo.toml. #[tokio::main] mengizinkan async fn main karena makro tersebut menuliskan ulang fungsi menjadi Runtime::new() + block_on — persis pola manual yang kita jalankan berdampingan dengan hasil output identik 3 detik. Future teratas berjalan di thread main, sementara armada thread worker sejumlah core CPU menunggu tugas yang kita luncurkan. Alat peluncurnya bernama tokio::spawn, dan itulah pembuka chapter berikutnya.