Async/Await di Rust
Bayangkan sebuah restoran. Di halaman Konkurensi, kamu sudah belajar tentang thread. Itu seperti mempekerjakan satu pelayan untuk setiap meja. Cara itu berhasil, tapi pelayan itu mahal, dan sebagian besar waktunya mereka cuma berdiri menunggu dapur.
Async itu seperti punya satu pelayan yang mencatat banyak pesanan. Pelayan mencatat pesanan dari meja 1, menyerahkannya ke dapur, dan selagi makanan dimasak, ia pergi ke meja 2. Tidak ada yang berdiri diam tanpa melakukan apa-apa. Itulah ide di balik async dan .await di Rust: selagi satu task (tugas) menunggu (jaringan, file, timer), task lain mendapat giliran berjalan.
Async paling bersinar saat programmu banyak menghabiskan waktu untuk menunggu: web server, klien API, aplikasi chat, bot. Halaman ini memakai tokio, runtime async paling populer untuk Rust.
Menyiapkan tokio
Section titled “Menyiapkan tokio”Standard library Rust memberimu kata kunci async dan .await, tapi tidak menyediakan “mesin” yang benar-benar menjalankan kode async. Mesin itu disebut runtime, dan tokio adalah yang dipakai sebagian besar proyek. Tambahkan ke proyek baru:
cargo new async_democd async_democargo add tokio --features fullPerintah itu menambahkan baris ini ke Cargo.toml-mu:
[dependencies]tokio = { version = "1", features = ["full"] }Fitur full menyalakan semuanya (timer, task, jaringan, dan lain-lain). Ini pilihan paling mudah selama kamu belajar. Tombol “Coba sendiri” di halaman ini juga berfungsi, karena Rust Playground sudah punya tokio bawaan.
Program async pertamamu
Section titled “Program async pertamamu”use std::time::Duration;
async fn make_coffee() -> String { tokio::time::sleep(Duration::from_millis(20)).await; String::from("kopi")}
#[tokio::main]async fn main() { println!("Memesan..."); let drink = make_coffee().await; println!("Ini {} pesananmu", drink);}Output:
Memesan...Ini kopi pesananmuAda tiga hal baru di sini:
async fnmembuat fungsi yang bisa berhenti sejenak di tengah jalan lalu melanjutkan nanti..awaitartinya “tunggu sampai ini selesai, dan selama menunggu, biarkan task lain berjalan”. Kamu hanya bisa memakai.awaitdi dalam fungsiasync.#[tokio::main]menyalakan runtime tokio dan menjalankanasync fn mainmilikmu di atasnya. Tanpa itu,maintidak bisaasync.
tokio::time::sleep adalah versi async dari “tunggu sebentar”. Ia hanya menghentikan task ini, bukan seluruh thread.
Future itu lazy
Section titled “Future itu lazy”Bagian inilah yang paling sering membuat orang kaget. Memanggil async fn tidak menjalankannya. Pemanggilan itu mengembalikan sebuah future, yaitu nilai yang berarti “pekerjaan yang akan dilakukan nanti”. Tidak ada yang terjadi sampai kamu memanggil .await padanya. Karena itulah future disebut lazy (malas):
async fn say_hello() { println!("Halo dari dalam future!");}
#[tokio::main]async fn main() { let future = say_hello(); // belum ada yang berjalan println!("Future sudah dibuat, tapi belum dimulai");
future.await; // sekarang baru berjalan println!("Selesai");}Output:
Future sudah dibuat, tapi belum dimulaiHalo dari dalam future!SelesaiAnggap future seperti kertas catatan pesanan. Menulis catatannya tidak membuat makanannya matang. Menyerahkannya ke dapur (.await) yang membuatnya dimasak.
Menjalankan beberapa task bersamaan dengan join!
Section titled “Menjalankan beberapa task bersamaan dengan join!”Sejauh ini kita menunggu satu hal dalam satu waktu. Kekuatan sebenarnya muncul saat kamu menunggu beberapa hal sekaligus. tokio::join! menjalankan beberapa future bersama-sama dan menunggu sampai semuanya selesai. Hasilnya dikembalikan dalam bentuk tuple:
use std::time::Duration;use tokio::time::sleep;
async fn fetch_user() -> String { sleep(Duration::from_millis(30)).await; // anggap saja ini panggilan jaringan String::from("Ana")}
async fn fetch_order_count() -> u32 { sleep(Duration::from_millis(20)).await; 3}
#[tokio::main]async fn main() { let (user, orders) = tokio::join!(fetch_user(), fetch_order_count()); println!("{} punya {} pesanan", user, orders);}Output:
Ana punya 3 pesananKalau kamu menunggu keduanya satu per satu, total waktu tunggunya sekitar 30 + 20 = 50 ms. Dengan join!, kedua penantian terjadi bersamaan, jadi hanya butuh sekitar 30 ms, yaitu waktu yang paling lama. Dengan panggilan jaringan sungguhan, selisih ini cepat sekali bertambah besar.
Membuat task dengan tokio::spawn
Section titled “Membuat task dengan tokio::spawn”join! sangat pas kalau jumlah future-nya tetap. Kalau kamu punya daftar pekerjaan, pakai tokio::spawn. Ia menyerahkan future ke runtime sebagai task tersendiri, langsung menjalankannya, dan memberimu sebuah JoinHandle. Nanti, kamu memanggil .await pada handle itu untuk mendapatkan hasilnya:
use std::time::Duration;use tokio::time::sleep;
async fn download(id: u64) -> String { sleep(Duration::from_millis(50 - id * 10)).await; format!("file {} selesai diunduh", id)}
#[tokio::main]async fn main() { let mut handles = Vec::new();
for id in 1..=3 { let handle = tokio::spawn(download(id)); // mulai berjalan sekarang handles.push(handle); }
for handle in handles { let result = handle.await.unwrap(); println!("{}", result); }}Output:
file 1 selesai diunduhfile 2 selesai diunduhfile 3 selesai diunduhPerhatikan baik-baik: file 3 punya waktu tidur paling pendek, jadi sebenarnya ia selesai paling dulu. Tapi kita mencetak hasilnya sesuai urutan kita menunggu handle-nya, jadi output-nya selalu 1, 2, 3. Ketiga unduhan tetap berjalan bersamaan.
Kenapa ada .unwrap()? Menunggu sebuah JoinHandle menghasilkan Result, karena task-nya mungkin saja panic. Kalau itu terjadi, kamu mendapat Err, bukan program yang crash seluruhnya. Ini memang sengaja dibuat mirip thread::spawn dan .join() dari halaman Konkurensi.
Kesalahan umum
Section titled “Kesalahan umum”Lupa .await. Karena future itu lazy, lupa .await berarti pekerjaannya tidak pernah dilakukan. Kalau kamu memakai nilainya, compiler akan menghentikanmu:
async fn get_number() -> u32 { 5}
#[tokio::main]async fn main() { let n = get_number(); // lupa .await println!("{}", n + 1);}error[E0369]: cannot add `{integer}` to `impl Future<Output = u32>` --> src/main.rs:8:22 |8 | println!("{}", n + 1); | - ^ - {integer} | | | impl Future<Output = u32>Pesan compiler (selalu dalam bahasa Inggris) ini bilang n adalah future dari u32, bukan u32. Perbaikannya: let n = get_number().await;. Kalau fungsinya tidak mengembalikan apa-apa dan kamu cuma memanggilnya, seperti save_file();, kamu hanya mendapat warning. Jadi, baca warning-mu:
warning: unused implementer of `Future` that must be used |7 | save_file(); | ^^^^^^^^^^^ | = note: futures do nothing unless you `.await` or poll themCatatan di baris terakhir artinya: future tidak melakukan apa-apa kecuali kamu memanggil .await atau mem-poll-nya.
Memakai std::thread::sleep di dalam kode async. Kode ini bisa di-compile tanpa masalah, dan itulah yang membuatnya licik:
async fn slow_task() { std::thread::sleep(std::time::Duration::from_secs(1)); // memblokir seluruh thread!}std::thread::sleep membekukan seluruh thread. Runtime tidak bisa berpindah ke task lain, jadi semua yang ada di thread itu ikut berhenti, seperti pelayan yang tertidur sambil berdiri di satu meja. Di dalam kode async, selalu pakai tokio::time::sleep(...).await. Hal yang sama berlaku untuk pekerjaan lambat lain yang memblokir, seperti membaca file besar dengan std::fs: tokio punya versi async-nya (tokio::fs), atau kamu bisa memakai tokio::task::spawn_blocking.
Berharap async membuat pekerjaan CPU lebih cepat. Async membantu saat task sedang menunggu. Async tidak membantu saat task sedang sibuk, seperti mengubah ukuran gambar atau mengolah angka. Satu pelayan tidak bisa memasak lebih cepat hanya karena mencatat lebih banyak pesanan. Untuk pekerjaan CPU yang berat, pakai thread (atau crate seperti rayon).
Async atau thread?
Section titled “Async atau thread?”| Pakai async kalau… | Pakai thread kalau… |
|---|---|
| Sebagian besar waktu habis untuk menunggu (jaringan, database, timer) | Pekerjaannya berupa perhitungan berat |
| Kamu butuh ribuan task sekaligus (seperti web server) | Kamu hanya butuh beberapa hal berjalan paralel |
| Kamu memakai crate async seperti reqwest atau axum | Kamu tidak mau menambah dependensi runtime |
Kamu tidak harus memilih salah satu saja. Banyak program sungguhan memakai async untuk jaringan dan thread untuk pekerjaan berat.
Butuh task yang saling mengirim pesan? Tokio punya channel sendiri, tokio::sync::mpsc. Cara kerjanya mirip channel standar di halaman Channel, bedanya kamu menulis rx.recv().await, jadi menunggu pesan tidak memblokir thread.
Apa yang dibangun orang dengan async
Section titled “Apa yang dibangun orang dengan async”Sebagian besar ekosistem async Rust dibangun di atas tokio. Ada dua crate yang akan kamu temui di mana-mana:
- reqwest (bahasa Inggris): klien HTTP untuk memanggil web API. Kamu menulis
reqwest::get(url).awaituntuk mengambil sebuah halaman. - axum (bahasa Inggris): web framework untuk membangun server dan API, dibuat oleh tim tokio.
Keduanya cocok dipasangkan dengan serde_json untuk membaca dan menulis JSON.
Latihan
Section titled “Latihan”1. Siapkan sarapan sekaligus
Section titled “1. Siapkan sarapan sekaligus”Tulis tiga fungsi async: make_toast (tidur 30 ms), fry_eggs (tidur 20 ms) dan brew_coffee (tidur 10 ms). Masing-masing mengembalikan String berisi nama makanannya. Jalankan ketiganya bersamaan lalu cetak apa saja yang sudah siap. Petunjuk: tokio::join! menerima beberapa future dan mengembalikan tuple berisi hasilnya.
Lihat solusi
use std::time::Duration;use tokio::time::sleep;
async fn make_toast() -> String { sleep(Duration::from_millis(30)).await; String::from("roti panggang")}
async fn fry_eggs() -> String { sleep(Duration::from_millis(20)).await; String::from("telur goreng")}
async fn brew_coffee() -> String { sleep(Duration::from_millis(10)).await; String::from("kopi")}
#[tokio::main]async fn main() { let (toast, eggs, coffee) = tokio::join!(make_toast(), fry_eggs(), brew_coffee()); println!("Sarapan: {}, {} dan {}", toast, eggs, coffee);}Output:
Sarapan: roti panggang, telur goreng dan kopiKopinya siap paling dulu, tapi tuple-nya selalu kembali sesuai urutan future yang kamu tulis. Seluruh sarapan hanya butuh sekitar 30 ms (hidangan paling lama), bukan 60 ms.
2. Mengkuadratkan angka di latar belakang
Section titled “2. Mengkuadratkan angka di latar belakang”Tulis async fn square_later(n: u64) -> u64 yang tidur selama 10 * n milidetik lalu mengembalikan n * n. Di main, buat satu task untuk setiap angka dari 1 sampai 4, lalu tunggu semuanya dan cetak totalnya. Petunjuk: masukkan setiap handle tokio::spawn(...) ke sebuah Vec, lalu lakukan perulangan atas handle-handle itu dan jumlahkan handle.await.unwrap().
Lihat solusi
use std::time::Duration;use tokio::time::sleep;
async fn square_later(n: u64) -> u64 { sleep(Duration::from_millis(10 * n)).await; n * n}
#[tokio::main]async fn main() { let mut handles = Vec::new(); for n in 1..=4 { handles.push(tokio::spawn(square_later(n))); }
let mut total = 0; for handle in handles { total += handle.await.unwrap(); } println!("total = {total}");}Output:
total = 30Keempat task langsung dimulai dan tidur bersamaan, jadi ini hanya butuh sekitar 40 ms, bukan 10 + 20 + 30 + 40 = 100 ms.
Ringkasan
Section titled “Ringkasan”- Async memungkinkan satu thread menangani banyak task dengan berpindah-pindah di antara mereka selagi task-task itu menunggu.
- Rust butuh runtime untuk menjalankan kode async. Tambahkan tokio dengan
cargo add tokio --features fulldan tandaimaindengan#[tokio::main]. async fnmengembalikan sebuah future. Future itu lazy: tidak ada yang berjalan sampai kamu memanggil.await.- Pakai
tokio::join!untuk menunggu beberapa future sekaligus, dantokio::spawnuntuk memulai task dan mendapatkanJoinHandle. - Jangan pernah memakai
std::thread::sleepdi kode async; pakaitokio::time::sleep(...).await. - Async itu untuk menunggu, bukan untuk pekerjaan CPU yang berat. Untuk itu, pakai thread.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa yang terjadi kalau kamu memanggil async fn tanpa .await?
2. Apa yang dilakukan #[tokio::main]?
3. Dua future tidur selama 30 ms dan 20 ms. Kira-kira berapa lama tokio::join! atas keduanya?
4. Kenapa std::thread::sleep bermasalah di dalam kode async?
5. Pekerjaan mana yang paling cocok untuk async?
Skor: 0 / 5
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Fundamentals of Asynchronous Programming — The Rust Book (bahasa Inggris): bab resmi tentang future,
async,.awaitdan stream. - The tokio tutorial (bahasa Inggris): panduan praktik yang membangun server kecil mirip Redis, mencakup task, state bersama, dan channel.
- tokio di docs.rs (bahasa Inggris): referensi API untuk
tokio::spawn,tokio::time,tokio::syncdan lainnya. - Asynchronous Programming in Rust (the async book) (bahasa Inggris): pembahasan lebih dalam tentang cara kerja future dan executor di balik layar.