Topik ini membahas Tokio, yaitu runtime async yang paling umum dipakai komunitas Rust dan menjadi dasar bagi banyak kerangka web serta klien HTTP. Pembahasannya mencakup peran runtime bagi future, bentuk yang dihasilkan anotasi #[tokio::main], pembuatan proyek Cargo dengan dependensi tokio, serta cara menjalankan pekerjaan secara serentak lewat tokio::spawn dan JoinHandle. Setelah mengikuti topik ini, program async pertama dapat dijalankan dengan benar, dan perbedaan antara menunggu di tempat dan menyerahkan pekerjaan ke runtime dapat diterapkan sesuai kebutuhan.
Peran runtime pada program async#
Future bersifat lazy sehingga tidak ada yang menjalankannya tanpa executor, dan Rust sengaja tidak menyediakan executor bawaan di pustaka standar. Tokio mengisi posisi itu dengan tiga komponen yang bekerja sama: scheduler yang mempoll task (future yang diserahkan kepada runtime) pada sejumlah worker thread, driver I/O yang memantau kesiapan soket lewat mekanisme seperti epoll, dan timer yang menjadwalkan wake berdasarkan waktu. Ketiga komponen itulah yang membuat penantian async benar-benar menyerahkan thread kepada pekerjaan lain, bukan sekadar memindahkan blokiran ke thread lain.
Anotasi #[tokio::main] menyembunyikan pembuatan runtime di balik main biasa. Bentuk ringkas yang dihasilkannya kurang lebih seperti berikut, dan memahaminya membantu karena isi async fn main sesungguhnya dieksekusi oleh block_on di dalam main sinkron.
// tidak-dijalankan
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { /* isi async fn main berada di sini */ })
}Panggilan enable_all mengaktifkan driver I/O dan timer agar tokio::net serta tokio::time dapat bekerja. Runtime multi-thread lalu menjalankan block_on untuk future utama dengan mekanisme yang sama seperti executor mini satu future, tetapi dilengkapi antrean task per worker thread dan kerja penjadwalan yang jauh lebih lengkap. Struktur runtime Tokio dirangkum oleh diagram berikut sebelum dipakai pada program pertama.
Proyek pertama dengan tokio#
Proyek contoh disiapkan di /tmp agar pohon konten situs tetap bersih dari direktori target. Berkas Cargo.toml pada bundle topik memuat dependensi tokio dengan tiga fitur yang dibutuhkan contoh: macros untuk anotasi #[tokio::main], rt-multi-thread untuk worker thread, dan time untuk sleep.
[package]
name = "async-runtime"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }Berkas main.rs memperagakan dua bentuk penantian. Penantian pertama dieksekusi langsung di dalam async fn main dengan sleep, sedangkan tiga pekerjaan berikutnya diserahkan ke runtime lewat task::spawn yang masing-masing mengembalikan JoinHandle. Berkas ini ditandai tidak-dijalankan karena bergantung pada crate tokio dan tidak dapat dikompilasi oleh rustc yang berdiri sendiri. Eksekusi yang sebenarnya berlangsung lewat Cargo pada blok bash di bawahnya.
// tidak-dijalankan
use std::time::Duration;
use tokio::task;
use tokio::time::sleep;
async fn gandakan(jeda: Duration, nilai: u32) -> u32 {
sleep(jeda).await;
nilai * 2
}
#[tokio::main]
async fn main() {
sleep(Duration::from_millis(10)).await;
println!("penantian pertama selesai");
let tugas1 = task::spawn(gandakan(Duration::from_millis(30), 10));
let tugas2 = task::spawn(gandakan(Duration::from_millis(10), 20));
let tugas3 = task::spawn(gandakan(Duration::from_millis(20), 30));
println!("task 1: {}", tugas1.await.unwrap());
println!("task 2: {}", tugas2.await.unwrap());
println!("task 3: {}", tugas3.await.unwrap());
}# mulai dari proyek baru di /tmp agar pohon konten situs tetap bersih
rm -rf /tmp/async-runtime
# buat proyek binary baru tanpa repositori git
cargo new /tmp/async-runtime -q --vcs none
# salin berkas proyek dari bundle topik ini
cp Cargo.toml /tmp/async-runtime/
cp main.rs /tmp/async-runtime/src/
# jalankan programnya dengan direktori target di luar proyek
CARGO_TARGET_DIR=/tmp/target-async cargo run -q --manifest-path /tmp/async-runtime/Cargo.tomlpenantian pertama selesai
task 1: 20
task 2: 40
task 3: 60Keluaran program bersifat deterministik meskipun ketiga task selesai dalam urutan yang berbeda dari urutan cetak. Task dengan jeda 10 ms selesai lebih dulu, disusul task 20 ms dan 30 ms, tetapi main menunggu JoinHandle satu per satu dalam urutan penulisan sehingga hasilnya tercetak sebagai task 1, 2, dan 3. Total penantian ketiga task kira-kira sama dengan jeda terpanjang karena ketiganya dijadwalkan serentak. Bila dijalankan berurutan, totalnya mencapai 60 ms. Pola ini, yaitu mengumpulkan hasil lewat JoinHandle lalu mencetak di satu tempat, adalah cara standar menjaga keluaran program konkuren tetap teratur.
Task dan JoinHandle#
Fungsi tokio::spawn menyerahkan sebuah future kepada runtime dan langsung mengembalikan JoinHandle<T> tanpa menunggu apa pun. Future yang diserahkan harus memenuhi dua syarat: bertanda Send agar dapat dipindahkan antar worker thread, dan berumur 'static sehingga tidak meminjam data dari lingkup pemanggil. Future yang tidak memenuhi kedua syarat itu ditolak oleh kompiler pada saat kompilasi, bukan menjadi bug tersembunyi saat program berjalan. Data yang dipakai bersama oleh beberapa task dibungkus Arc, sedangkan data yang cukup dipindahkan kepemilikannya diambil dengan move.
JoinHandle adalah future yang menghasilkan Result<T, JoinError> ketika di-await. Cabang Ok memuat nilai kembalian task, sedangkan cabang Err muncul bila task panik sebelum selesai, sehingga unwrap pada contoh melanjutkan panik itu ke pemanggil. Menunggu handle bersifat sukarela: task tetap berjalan meskipun handle diabaikan, dan program dapat berakhir lebih dulu tanpa menunggu task yang masih sibuk. Untuk kerja yang memblokir thread seperti panggilan pustaka lama atau kompresi berkas besar, Tokio menyediakan spawn_blocking yang menempatkan pekerjaan pada thread khusus agar worker thread tidak lumpuh.
Kesalahan umum#
Kesalahan pertama adalah mendeklarasikan tokio tanpa fitur yang dibutuhkan, misalnya tokio = "1" tanpa features. Anotasi #[tokio::main] memerlukan fitur macros dan rt, sedangkan sleep memerlukan time, sehingga fitur yang hilang muncul sebagai error kompilasi yang menyebut makro atau fungsi tidak ditemukan. Tambahkan fitur yang diperlukan satu per satu, atau gunakan fitur full pada tahap eksperimen.
Kesalahan kedua adalah menyerahkan future yang meminjam variabel lokal, misalnya hasil async fn yang menerima &String dari lingkup main. Kompiler menolaknya karena task berumur 'static sedangkan pinjaman itu bisa lebih pendek. Pindahkan kepemilikan data ke dalam task dengan move, atau bungkus data bersama dalam Arc.
Kesalahan ketiga adalah mengharapkan urutan keluaran dari task yang berjalan serentak. Baris yang dicetak dari dalam beberapa task dapat muncul dalam urutan apa pun, dan urutan itu berubah antar-jalankan. Kumpulkan hasil lewat JoinHandle, lalu cetak secara berurutan dari satu tempat seperti contoh di atas.
Ringkasan#
- Runtime menyediakan executor, driver I/O, dan timer, karena future yang lazy tidak berjalan tanpa keduanya.
- Anotasi
#[tokio::main]membangun runtime multi-thread lalu menjalankan isi async main lewatblock_on. tokio::spawnmenyerahkan future ber-Senddan'statickepada scheduler serta mengembalikanJoinHandleyang dapat di-await untuk mengambil hasil.- Menunggu handle dalam urutan penulisan menjaga keluaran tetap teratur meskipun task selesai dalam urutan yang berbeda.
Referensi#
- Tokio tutorial — tokio.rs — Tutorial resmi yang membahas runtime, spawn, dan task secara berurutan.
- tokio::spawn — docs.rs
— Dokumentasi fungsi spawn beserta syarat
Senddan'static. - tokio::task — docs.rs
— Modul task termasuk
JoinHandle,spawn_blocking, danJoinSet. - tokio::runtime::Builder — docs.rs
— API pembangunan runtime yang diperluas oleh anotasi
#[tokio::main].