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:

FeatureIsi yang diaktifkan
rtinti runtime dan block_on
rt-multi-threadscheduler multi-thread dengan thread worker
macrosmakro #[tokio::main], join!, dan select!
timetimer sleep, timeout
fsoperasi berkas tokio::fs
netjaringan tokio::net (TcpListener, TcpStream)
io-utiltrait AsyncReadExt dan AsyncWriteExt
syncprimitif 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:

toml
[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:

rust
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:

text
berjalan di thread Some("main")
daftar harga selesai
daftar stok selesai
total: 3 detik

Mari kita bedah kode di atas:

  1. Baris 8 (#[tokio::main]): atribut ini memicu ekspansi makro sebelum kompilasi — tubuh async fn main dibungkus menjadi runtime + block_on, persis pola manual yang kita tulis sendiri di chapter-chapter sebelumnya.
  2. Baris 10 (println!("berjalan di thread {:?}", ...)): std::thread::current().name() menanyakan identitas thread yang sedang menjalankan kode. Hasilnya Some("main") — future teratas dari block_on dijalankan di thread pemanggilnya, bukan di thread worker.
  3. Baris 13-14 (download_data(...).await dua kali): dua unduhan berdurasi 1 dan 2 detik ditunggu berurutan, sehingga totalnya 3 detik. Runtime sudah aktif, tetapi .await yang berurutan tetap membuat penantinan berbaris seri — mesinnya berbeda dari Chapter Mengapa Async, hasil waktunya sama.
  4. 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:

rust
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:

text
daftar harga selesai
daftar stok selesai
total: 3 detik

Mari kita bedah perbedaannya terhadap versi beratribut:

  1. 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].
  2. Baris 11-18 (runtime.block_on(async { ... })): blok async menggantikan tubuh async fn main, dan block_on menggerakkannya sampai selesai di thread main. Output kedua program identik — bukti makro hanyalah penulis otomatis pola ini.
  3. Variabel runtime hidup sampai akhir main: selama variabel tersebut belum keluar scope, scheduler tetap hidup. Aturan kepemilikan dari Section Ownership berlaku seperti biasa: drop runtime berarti 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.