Future dari Chapter sebelumnya adalah janji yang malas: dibuat murah, tidak bergerak sendiri. Operator .await adalah sisi pelengkapnya — titik di mana kode kita menyatakan “berhenti di sini sampai hasilnya ada, tetapi thread tidak boleh ikut berhenti”. Inilah perbedaan paling mendasar antara async dan model thread di Section Koncurrency: thread::sleep menidurkan seluruh thread, sementara .await hanya mengistirahatkan task dan meminjamkan thread kepada pekerjaan lain. Chapter ini membuktikan klaim itu dengan dua program berdurasi terukur, lalu menyambut dua jebakan klasik pemula: .await di luar konteks async dan future yang lupa di-await.
Menunggu Hasil Tanpa Memblokir Thread #
Secara tampilan, .await membuat kode async terlihat seperti kode sinkron biasa: let data = fetch_data("gudang").await; terbaca “panggil fungsi, tunggu, simpan hasilnya”. Mekanisme di balik tanda titik itu jauh lebih sopan terhadap thread. Ketika executor meng-poll task dan bertemu .await di mana future masih Pending, task mengembalikan jawaban Pending ke executor — artinya task menyerahkan kendali thread dan berstatus “panggil saya lagi ketika datanya siap”. Thread worker kemudian bebas menggarap task lain. Ketika data akhirnya tiba (event terdeteksi, timer menyala), task dijadwalkan ulang dan melanjutkan eksekusi tepat setelah .await, dengan nilai hasil di tangan.
Program berikut menyusun dua future berurutan dan mengukur total waktunya:
use std::time::{Duration, Instant};
async fn fetch_data(source: &str) -> String {
// simulasi jeda jaringan selama 1 detik
tokio::time::sleep(Duration::from_secs(1)).await;
format!("data dari {}", source)
}
async fn build_report() -> String {
let data_a = fetch_data("gudang").await;
let data_b = fetch_data("kasir").await;
format!("{} dan {}", data_a, data_b)
}
fn main() {
let start = Instant::now();
let runtime = tokio::runtime::Runtime::new().unwrap();
let report = runtime.block_on(build_report());
println!("{}", report);
println!("total: {} detik", start.elapsed().as_secs());
}Output yang Dihasilkan:
data dari gudang dan data dari kasir
total: 2 detikMari kita bedah kode di atas:
- Baris 4-6 (tubuh
fetch_data):tokio::time::sleepmengembalikan future timer yang selesai setelah durasi tertentu;.awaitdi baris 5 menjadikannya jeda non-blocking — task beristirahat 1 detik tanpa menidurkan thread. - Baris 10 (
let data_a = fetch_data("gudang").await;):build_reportberhenti di titik ini sampai data gudang tiba. Karena.awaitkedua belum tercapai, jeda kasir belum dimulai — dua penantinan berbaris seri, bukan paralel. - Baris 11 (
let data_b = fetch_data("kasir").await;): baru setelah data pertama di tangan, penantinan kedua dibangun dan ditunggu. Hasilnya: 1 detik + 1 detik = 2 detik, persis seperti tertera di output. - Baris 18 (
runtime.block_on(build_report())):block_onmenggerakkan task paling atas; di dalamnya, setiap.awaitadalah kesempatan task menyerahkan kendali. Ketika hanya ada satu task, thread memang menganggur saat menunggu — kelihatannya mirip blocking, tetapi mesinnya berbeda: kapasitas menyerahkan kendali tetap terpasang dan akan terpakai begitu task lain hadir.
Total 2 detik memang masih serial. Bukti sesungguhnya bahwa .await tidak memblok thread justru muncul ketika dua future ditunggu serentak.
Bukti Thread Tidak Terblokir dengan join! #
Makro tokio::join! menerima beberapa future dan menjalankannya bergantian di satu task yang sama: setiap kali salah satu future menjawab Pending, future lain ikut di-poll. Dua jeda 1 detik yang tadinya berbaris kini tumpang tindih dalam satu thread:
use std::time::{Duration, Instant};
async fn fetch_data(source: &str) -> String {
// simulasi jeda jaringan selama 1 detik
tokio::time::sleep(Duration::from_secs(1)).await;
format!("data dari {}", source)
}
fn main() {
let start = Instant::now();
let runtime = tokio::runtime::Runtime::new().unwrap();
let (data_a, data_b) = runtime.block_on(async {
tokio::join!(fetch_data("gudang"), fetch_data("kasir"))
});
println!("{} dan {}", data_a, data_b);
println!("total: {} detik", start.elapsed().as_secs());
}Output yang Dihasilkan:
data dari gudang dan data dari kasir
total: 1 detikMari kita bedah perubahan pentingnya:
- Baris 12-14 (
runtime.block_on(async { ... })): blokasync { }adalah task anonim — tubuhnya boleh memakai.awaitlayaknyaasync fn.block_onmenggerakkan blok ini di thread pemanggilnya. - Baris 13 (
tokio::join!(fetch_data("gudang"), fetch_data("kasir"))):join!membangun kedua future sekaligus lalu meng-pollnya bergantian. Saat future gudang menjawabPendingdi tengah jeda 1 detiknya, future kasir mendapat giliran memulai jeda 1 detiknya — keduanya berjalan serentak pada thread yang sama. - Baris 12 (
let (data_a, data_b) = ...):join!mengembalikan tuple hasil semua future-nya setelah semuanya selesai, sehingga kedua nilai tersedia bersamaan. - Output
total: 1 detik: dua penantinan 1 detik tumpang tindih di satu thread — pengukuran turun dari 2 detik ke 1 detik tanpa satu thread tambahan pun. Jikathread::sleepyang dipakai, thread pasti terblok penuh dan tumpang tindih seperti ini mustahil terjadi tanpa thread kedua.
.await Hanya Sah di Dalam Konteks async #
Kesalahan pertama pemula: memakai .await di fungsi biasa yang tidak bertanda async. Kompilator menolak dengan E0728:
use std::time::Duration;
fn main() {
tokio::time::sleep(Duration::from_secs(1)).await;
println!("selesai menunggu");
}Pesan galat kompilator:
error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this is not `async`
4 | tokio::time::sleep(Duration::from_secs(1)).await;
| ^^^^^ only allowed inside `async` functions and blocks
For more information about this error, try `rustc --explain E0728`.
error: could not compile `belajar-async` (bin "belajar-async") due to 1 previous errorAlasannya melekat pada mekanisme yang baru saja kita bedah: menyerahkan kendali ke executor hanya bermakna jika ada state machine yang mencatat titik berhenti, dan state machine hanya dibangun di dalam async fn atau blok async { }. Fungsi biasa tidak punya kedua hal itu, sehingga .await tidak punya arti. Perbaikannya sesederhana menambahkan kata async pada fungsi — atau memindahkan pemanggilan ke fungsi async lain — lalu memastikan ada runtime yang menggerakkan fungsi tersebut.
Future yang Lupa Di-await #
Jebakan kedua lebih licik karena tidak menghentikan kompilasi. Bayangkan kita memanggil fetch_data tanpa .await:
use std::time::Duration;
async fn fetch_data(source: &str) -> String {
tokio::time::sleep(Duration::from_secs(1)).await;
format!("data dari {}", source)
}
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
fetch_data("gudang");
println!("program selesai");
});
}Pesan warning kompilator:
warning: unused implementer of `Future` that must be used
--> src/main.rs:11:9
|
11 | fetch_data("gudang");
| ^^^^^^^^^^^^^^^^^^^^
|
= note: futures do nothing unless you `.await` or poll them
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
warning: `belajar-async` (bin "belajar-async") generated 1 warningProgram tetap berjalan dan mencetak program selesai hampir seketika — tanpa jeda 1 detik dan tanpa data gudang, karena future yang dibangun di baris 11 tidak pernah di-poll siapa pun. Warning di atas berasal dari anotasi #[must_use] pada trait Future: kompilator tahu nilai ini sia-sia jika tidak di-await, lalu mengingatkan kita dengan pesan “futures do nothing unless you .await or poll them”. Perbaikannya: tambahkan .await (dan gunakan hasilnya), atau simpan future untuk dijalankan secara terpisah di chapter-chapter selanjutnya.
Ringkasan #
.await mengubah penantinan menjadi titik serah kendali: task menjawab Pending kepada executor, thread bekerja untuk task lain, dan eksekusi dilanjutkan tepat setelah hasil tersedia. Program pertama menunjukkan dua .await berurutan menumpuk menjadi 2 detik; tokio::join! kemudian menurunkannya menjadi 1 detik di thread yang sama — bukti nyata thread tidak terblok. E0728 menjaga .await tetap berada di dalam async fn atau blok async { }, sementara warning unused implementer of Future menjadi alarm untuk future yang terbuang percuma. Seluruh mekanisme ini bergantung pada satu aktor yang belum kita perkenalkan sungguhan: executor yang meng-poll future. Chapter berikutnya membuka mesin itu dengan runtime tokio.