Futures dan Sintaks Async
Elemen kunci dari pemrograman asinkron di Rust adalah futures dan keyword
async dan await milik Rust.
Sebuah future adalah nilai yang mungkin belum siap sekarang tapi bakal jadi
siap di suatu waktu di masa depan. (Konsep yang sama ini juga muncul di banyak
bahasa lain, kadang-kadang pakai nama lain kayak task atau promise.) Rust
menyediakan trait Future sebagai blok penyusun supaya berbagai operasi asinkron
bisa diimplementasikan pakai struktur data yang berbeda-beda tapi dengan
antarmuka (interface) yang sama. Di Rust, futures adalah tipe-tipe yang
mengimplementasikan trait Future. Tiap future menyimpan informasinya sendiri
soal sejauh mana kemajuan yang sudah dibuat dan apa makna dari “siap” (“ready”).
Kita bisa menerapkan keyword async ke blok dan fungsi buat menentukan kalau
mereka bisa diinterupsi dan dilanjutkan lagi. Di dalam sebuah blok asinkron atau
fungsi asinkron, kita bisa memakai keyword await buat menunggu sebuah future
(yaitu, nunggu dia sampai jadi siap). Titik mana pun di mana kita me-await
sebuah future di dalam sebuah blok atau fungsi asinkron adalah titik potensial
buat blok atau fungsi asinkron itu buat berhenti sejenak (pause) dan dilanjutkan
lagi (resume). Proses pengecekan ke sebuah future buat melihat apakah nilainya
sudah tersedia atau belum ini disebut polling.
Beberapa bahasa pemrograman lain, kayak C# dan JavaScript, juga memakai keyword
async dan await buat pemrograman asinkron. Kalau kita sudah familier sama
bahasa-bahasa itu, kita mungkin bakal sadar ada beberapa perbedaan signifikan
dari cara Rust melakukan hal tersebut, termasuk cara nanganin sintaksnya. Itu ada
alasan bagusnya lho, kayak yang bakal kita lihat nanti!
Pas lagi nulis Rust asinkron, kita bakal memakai keyword async dan await
sebagian besar waktunya. Rust mengompilasi mereka jadi kode yang ekuivalen
menggunakan trait Future, mirip kayak gimana dia mengompilasi for loops jadi
kode yang ekuivalen memakai trait Iterator. Tapi karena Rust menyediakan trait
Future, kita juga bisa mengimplementasikannya buat tipe data kita sendiri
kalau kita butuh. Banyak fungsi yang bakal kita lihat di sepanjang bab ini
mengembalikan tipe yang punya implementasi Future-nya masing-masing. Kita bakal
balik lagi ke definisi trait-nya di akhir bab ini dan gali lebih dalam soal
gimana cara kerjanya, tapi detail segini sudah cukup buat kita lanjut jalan dulu.
Ini semua mungkin terasa agak abstrak, jadi mari kita tulis program asinkron pertama kita: sebuah web scraper mini. Kita bakal memasukkan dua URL dari command line, mengambil (fetch) kedua URL itu secara konkuren, terus mengembalikan hasil dari URL mana pun yang selesai duluan. Contoh ini bakal punya lumayan banyak sintaks baru, tapi jangan khawatir—kita bakal jelasin semua yang perlu kita tahu seiring kita berjalan.
Program Asinkron Pertama Kita
Biar fokus bab ini tetap pada belajar asinkron ketimbang sibuk ngerjain bagian-
bagian ekosistem, kita sudah ngebikin crate trpl (trpl itu singkatan dari
“The Rust Programming Language”). Crate ini mengekspor ulang (re-exports) semua
tipe, trait, dan fungsi yang bakal kita butuhkan, utamanya dari crate
futures dan tokio.
Crate futures adalah tempat resmi buat bereksperimen dengan kode asinkron di
Rust, dan sebenarnya di sanalah trait Future asal-muasalnya didesain. Tokio
adalah async runtime yang paling banyak dipakai di Rust saat ini, apalagi buat
aplikasi web. Ada banyak runtimes keren lainnya di luar sana, dan mereka
mungkin lebih cocok buat kebutuhan kita. Kita memakai crate tokio di balik
layarnya trpl karena dia sudah teruji dengan baik dan banyak dipakai.
Di beberapa kasus, trpl juga mengganti nama atau membungkus (wraps) API aslinya
biar kita tetap fokus sama detail-detail yang relevan buat bab ini. Kalau kita
mau paham apa yang dilakukan sama crate ini, kita menyarankan kita buat cek
source code-nya. Kita bakal bisa lihat dari crate mana asal
dari tiap fitur yang di-re-export, dan kita sudah ninggalin komentar yang
ekstensif yang menjelaskan apa aja yang dilakukan sama crate tersebut.
Bikin sebuah project binary baru bernama hello-async dan tambahkan crate
trpl sebagai dependensi:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
Sekarang kita bisa pakai berbagai potongan yang disediakan sama trpl buat
nulis program asinkron pertama kita. Kita bakal bikin alat command line mini
yang mengambil dua halaman web, menarik elemen <title> dari masing-masing
halaman, terus mencetak judul dari halaman mana pun yang menyelesaikan seluruh
proses tersebut paling duluan.
Mendefinisikan Fungsi page_title
Mari mulai dengan nulis fungsi yang menerima satu URL halaman sebagai parameternya, melakukan request ke sana, dan mengembalikan teks dari elemen judulnya (lihat Listing 17-1).
extern crate trpl; // required for mdbook test
fn main() {
// TODO: we'll add this next!
}
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
Pertama, kita mendefinisikan sebuah fungsi bernama page_title dan menandainya
pakai keyword async. Terus kita pakai fungsi trpl::get buat mengambil
URL apa pun yang dimasukkan dan menambahkan keyword await buat menunggu
responsnya. Buat dapat teks dari respons tersebut, kita memanggil method text-
nya, dan sekali lagi menunggunya dengan keyword await. Kedua langkah ini
sifatnya asinkron. Buat fungsi get, kita harus nunggu server buat mengirim
balik bagian pertama dari responsnya, yang mana bakal berisi HTTP headers,
cookies, dan lain-lain, dan bisa saja dikirim secara terpisah dari body
(isi utama) responsnya. Apalagi kalau body-nya itu besar sekali, itu bisa
memakan waktu yang lumayan lama buat semuanya sampai. Karena kita harus nunggu
buat keseluruhan responsnya sampai, method text itu juga asinkron.
Kita harus secara eksplisit menunggu kedua futures ini, karena futures di
Rust itu lazy (malas): mereka tidak bakal melakukan apa-apa sampai kita
menyuruh mereka dengan keyword await. (Faktanya, Rust bakal mengeluarkan
peringatan compiler kalau kita tidak menunggu sebuah future.) Ini mungkin
mengingatkan kita soal pembahasan iterator di Bab 13 di bagian “Memproses
Serangkaian Item dengan Iterator”. Iterator
tidak bakal melakukan apa-apa kecuali kalau kita memanggil method next-nya—baik
itu secara langsung atau dengan memakai for loops atau method-method kayak
map yang memakai next di balik layarnya. Demikian juga, futures tidak
bakal melakukan apa-apa kecuali kita menyuruh mereka secara eksplisit. Sifat
malas ini membolehkan Rust menghindari menjalankan kode asinkron sampai dia
bener-bener dibutuhkan.
Catatan: Ini berbeda dari perilaku yang kita lihat di Bab 16 saat memakai
thread::spawndi “Membikin Thread Baru dengan spawn”<!–ignore –>, di mana closure yang kita kasih ke thread lain itu langsung mulai berjalan. Ini juga beda dari cara banyak bahasa lain melakukan pendekatan ke asinkron. Tapi penting buat Rust buat bisa ngasih jaminan performanya, sama halnya dengan iterator.
Begitu kita dapet response_text, kita bisa menguraikan (parse) nilainya jadi
sebuah instance dari tipe Html menggunakan Html::parse. Daripada pakai
string mentah, sekarang kita punya sebuah tipe data yang bisa kita pakai buat
mengolah HTML tersebut sebagai struktur data yang lebih kaya. Secara spesifik,
kita bisa pakai method select_first buat menemukan instance pertama dari CSS
selector yang kita kasih. Dengan memasukkan string "title", kita bakal dapet
elemen <title> pertama di dokumen tersebut, kalau memang ada. Karena mungkin
saja nggak ada elemen yang cocok, select_first mengembalikan Option<ElementRef>.
Terakhir, kita memakai method Option::map, yang membolehkan kita beroperasi
pada item di dalam Option kalau itemnya ada, dan tidak melakukan apa-apa
kalau itemnya nggak ada. (Kita juga bisa saja pakai ekspresi match di sini,
tapi map itu lebih idiomatik.) Di dalam isi fungsi yang kita berikan ke map,
kita memanggil inner_html pada title buat mendapatkan kontennya, yang mana
adalah sebuah String. Pada akhirnya, kita punya sebuah Option<String>.
Perhatikan bahwa keyword await di Rust ditaruh setelah ekspresi yang lagi
kita tungguin, bukan di sebelumnya. Yakni, ia adalah sebuah keyword postfix
(akhiran). Ini mungkin beda dari apa yang biasa kita jumpai kalau kita pernah
memakai async di bahasa lain, tapi di Rust ini bikin chaining (rentetan)
pemanggilan method jadi jauh lebih enak buat dibuat. Hasilnya, kita bisa
mengubah isi dari page_title buat menyambung (chain) pemanggilan fungsi
trpl::get dan text sekaligus pakai await di antara mereka, kayak yang
ditunjukkan di Listing 17-2.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
// TODO: we'll add this next!
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
awaitSelesai deh, kita sudah berhasil menulis fungsi asinkron pertama kita! Sebelum
kita nambahin beberapa kode di main buat manggil dia, mari kita ngomongin
lebih lanjut soal apa yang sudah kita tulis ini dan apa maknanya.
Pas Rust melihat ada blok yang ditandai pakai keyword async, dia
mengompilasinya jadi tipe data anonim unik yang mengimplementasikan trait
Future. Pas Rust melihat fungsi ditandai pakai async, dia mengompilasinya
jadi fungsi non-asinkron yang isinya merupakan sebuah blok asinkron. Tipe
kembalian fungsi asinkron adalah tipe dari tipe data anonim yang dibuat sama
compiler buat blok asinkron tersebut.
Jadi, menulis async fn itu ekuivalen (sama aja) kayak menulis fungsi yang
mengembalikan sebuah future dari tipe kembaliannya. Bagi compiler, definisi
fungsi kayak async fn page_title di Listing 17-1 itu kira-kira ekuivalen
sama fungsi non-asinkron yang didefinisikan kayak gini:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
}
Mari kita telusuri bagian demi bagian dari versi yang sudah diubah ini:
- Dia memakai sintaks
impl Traityang sudah kita bahas dulu di Bab 10 di bagian “Traits sebagai Parameter”. - Nilai yang dikembalikan mengimplementasikan trait
Futuredengan associated typeOutput. Perhatikan bahwa tipeOutput-nya adalahOption<String>, yang mana sama dengan tipe kembalian asli dari versiasync fnsipage_title. - Semua kode yang dipanggil di dalam isi dari fungsi aslinya dibungkus di dalam
sebuah blok
async move. Ingat kembali kalau blok itu adalah ekspresi. Keseluruhan blok ini adalah ekspresi yang dikembalikan dari fungsinya. - Blok asinkron ini menghasilkan nilai bertipe
Option<String>, seperti yang baru saja dijelaskan. Nilai tersebut cocok sama tipeOutputdi tipe kembaliannya. Ini sama persis kayak blok-blok lain yang pernah kita lihat. - Isi fungsi baru tersebut adalah sebuah blok
async movegara-gara gimana dia memakai parameterurl. (Kita bakal ngebahas lebih banyak lagi soalasyncversusasync movenanti di bab ini.)
Sekarang kita bisa memanggil page_title di main.
Mengeksekusi Fungsi Asinkron dengan sebuah Runtime
Sebagai awalan, kita cuma bakal mengambil judul buat satu halaman saja, yang ditunjukkan di Listing 17-3. Sayangnya, kode ini belum bisa di-compile.
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
page_title dari main memakai argumen yang dikasih sama userKita mengikuti pola yang sama yang kita pakai buat dapat argumen command line
di Bab 12 di bagian “Menerima Argumen Command Line”.
Terus kita mengoper URL pertamanya ke page_title dan menunggu hasilnya.
Karena nilai yang dihasilkan oleh future tersebut adalah sebuah
Option<String>, kita memakai ekspresi match buat mencetak pesan yang beda-
beda dengan memperhitungkan apakah halamannya punya <title> atau tidak.
Satu-satunya tempat di mana kita bisa memakai keyword await adalah di dalam
fungsi atau blok asinkron, dan Rust tidak bakal membolehkan kita menandai
fungsi spesial main sebagai async.
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
Alasan main tidak bisa ditandai async adalah karena kode asinkron itu
butuh sebuah runtime: sebuah crate Rust yang mengelola detail eksekusi kode
asinkron. Fungsi main di sebuah program bisa menginisialisasi (initialize)
sebuah runtime, tapi fungsi main itu bukanlah sebuah runtime itu sendiri.
(Kita bakal lihat lebih lanjut soal kenapa ini terjadi sebentar lagi.) Setiap
program Rust yang mengeksekusi kode asinkron punya minimal satu tempat di mana
dia menyiapkan sebuah runtime yang mengeksekusi futures-nya.
Kebanyakan bahasa yang mendukung asinkron sudah membundel sebuah runtime bawaan, tapi Rust tidak begitu. Sebaliknya, ada banyak async runtimes berbeda yang tersedia, di mana masing-masing membikin tradeoffs (pertukaran) yang cocok buat kasus penggunaan yang jadi targetnya. Misalnya, web server yang high-throughput (kemampuan transmisi besar) dengan banyak core CPU dan RAM dalam jumlah besar punya kebutuhan yang sangat berbeda dari mikrokontroler dengan single core, jumlah RAM yang kecil, dan tidak punya kemampuan alokasi heap sama sekali. Crate yang menyediakan runtimes ini juga sering kali memberikan versi asinkron dari fungsionalitas umum kayak I/O file atau jaringan.
Di sini, dan di sepanjang sisa bab ini, kita bakal memakai fungsi block_on
dari crate trpl, yang mana menerima sebuah future sebagai argumen dan
memblokir thread saat ini sampai future tersebut berjalan hingga selesai.
Di balik layar, memanggil block_on menyiapkan sebuah runtime menggunakan
crate tokio yang dipakai buat menjalankan future yang diberikan (perilaku
block_on dari crate trpl ini mirip dengan fungsi block_on milik crate
runtime lainnya). Setelah future tersebut selesai, block_on bakal
mengembalikan nilai apa pun yang dihasilkan oleh future itu.
Kita bisa saja meneruskan future yang dikembalikan sama page_title langsung
ke block_on dan, begitu selesai, kita bisa melakukan match pada
Option<String> hasilnya, kayak yang sudah kita coba lakukan di Listing 17-3.
Tapi, buat mayoritas contoh di bab ini (dan mayoritas kode asinkron di dunia
nyata), kita bakal melakukan lebih dari sekadar satu pemanggilan fungsi
asinkron saja, jadi alih-alih begitu kita bakal meneruskan sebuah blok async
lalu secara eksplisit menunggu hasil dari panggilan page_title, seperti di
Listing 17-4.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
trpl::block_onPas kita jalankan kode ini, kita dapet perilaku kayak yang kita harapkan di awal:
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
Fiuh—akhirnya kita punya kode asinkron yang jalan! Tapi sebelum kita nambahin kode buat mengadu (race) kedua situs web itu satu sama lain, mari kita alihkan sejenak perhatian kita kembali ke gimana futures itu bekerja.
Setiap await point (titik penantian)—yakni, setiap tempat di mana kodenya
memakai keyword await—merepresentasikan sebuah tempat di mana kontrol
dikembalikan ke runtime. Biar itu bisa terjadi, Rust perlu melacak (keep
track of) state (keadaan) yang terlibat di dalam blok asinkron tersebut
sehingga runtime bisa memulai beberapa pekerjaan lain lalu balik lagi nanti
kalau dia sudah siap buat mencoba melanjutkan pekerjaan pertama tadi. Ini
adalah sebuah state machine (mesin keadaan) kasatmata, seolah-olah kita
menulis sebuah enum kayak gini buat menyimpan state saat ini di tiap titik
await:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
}
Menulis kode buat bertransisi di antara setiap state ini secara manual bakal melelahkan dan gampang rawan error, apalagi kalau nanti kita harus menambahkan fungsionalitas dan lebih banyak states lagi ke kode tersebut. Untungnya, compiler Rust otomatis membikin dan mengelola struktur data state machine buat kode asinkron. Aturan-aturan borrowing dan ownership normal seputar struktur data itu tetap berlaku semua, dan syukurnya, compiler juga menangani pengecekan itu buat kita dan menyediakan pesan error yang berguna. Kita bakal membedah beberapa kasus kayak gitu nanti di bab ini.
Pada akhirnya, sesuatu harus mengeksekusi state machine ini, dan “sesuatu” itu adalah sebuah runtime. (Inilah kenapa kita mungkin pernah ketemu istilah executors (pengeksekusi) pas lagi nyari tahu soal runtimes: sebuah executor adalah bagian dari sebuah runtime yang bertugas mengeksekusi kode asinkron tersebut.)
Sekarang kita bisa tahu kenapa compiler melarang kita membikin main itu
sendiri jadi fungsi asinkron balik di Listing 17-3 tadi. Kalau main itu fungsi
asinkron, sesuatu yang lain bakal harus mengelola state machine buat future
apa pun yang dikembalikan sama main, tapi padahal main adalah titik awal
buat programnya! Sebaliknya, kita memanggil fungsi trpl::block_on di main
buat menyiapkan sebuah runtime dan menjalankan future yang dikembalikan
sama blok async sampai dia selesai.
Catatan: Beberapa runtimes menyediakan macros sehingga kita bisa menulis fungsi
mainyang asinkron. Macros itu menulis ulangasync fn main() { ... }jadifn mainnormal, yang melakukan persis hal yang sama kayak yang kita lakukan secara manual di Listing 17-4: memanggil fungsi yang mengeksekusi sebuah future sampai selesai kayak yang dilakukan samatrpl::block_on.
Sekarang mari kita gabungkan bagian-bagian ini dan lihat gimana kita bisa menulis kode konkuren.
Menandingkan (Racing) Dua URL Secara Konkuren
Di Listing 17-5, kita memanggil page_title dengan dua URL berbeda yang
dimasukkan dari command line lalu mengadu (race) mereka berdua dengan
memilih future mana pun yang selesai duluan.
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
page_title buat dua URL untuk melihat mana yang kembali duluanKita mulai dengan memanggil page_title buat tiap URL yang dikasih sama user.
Kita simpan futures hasilnya sebagai title_fut_1 dan title_fut_2. Ingat,
mereka ini belum melakukan apa-apa, karena futures itu sifatnya malas dan
kita belum menunggunya. Terus kita mengoper futures tersebut ke
trpl::select, yang mengembalikan sebuah nilai buat mengindikasikan future
mana yang selesai duluan di antara yang dioper kepadanya.
Catatan: Di balik layar,
trpl::selectdibangun di atas fungsiselectyang lebih umum yang didefinisikan di cratefutures. Fungsiselectmilik cratefuturesbisa melakukan banyak hal yang fungsitrpl::selecttidak bisa, tapi dia juga punya kerumitan ekstra yang bisa kita lewati dulu buat sekarang.
Masing-masing future bisa saja “menang,” jadi tidak masuk akal kalau kita
mengembalikan Result. Alih-alih begitu, trpl::select mengembalikan sebuah
tipe yang belum pernah kita lihat sebelumnya, yaitu trpl::Either. Tipe
Either itu agak mirip sama Result dalam hal dia punya dua kasus. Bedanya
sama Result, tidak ada konsep “sukses” atau “gagal” yang tertanam di dalam
Either. Alih-alih begitu, dia memakai Left (kiri) dan Right (kanan) buat
mengindikasikan “yang satu atau yang lainnya”:
#![allow(unused)]
fn main() {
enum Either<A, B> {
Left(A),
Right(B),
}
}
Fungsi select mengembalikan Left yang berisi output dari future tersebut
kalau argumen pertama menang, dan Right yang berisi output future kedua
kalau yang itu yang menang. Ini cocok dengan urutan munculnya argumen-argumen
tersebut saat memanggil fungsinya: argumen pertama ada di kiri argumen kedua.
Kita juga memperbarui page_title buat mengembalikan URL yang sama dengan yang
dimasukkan. Dengan begitu, kalau halaman yang kembali duluan tidak punya
<title> yang bisa kita uraikan, kita masih bisa mencetak pesan yang bermakna.
Dengan informasi yang sudah tersedia itu, kita selesaikan ini semua dengan
mengubah output println! kita buat mengindikasikan baik URL mana yang selesai
duluan, dan apa, kalau memang ada, <title> buat halaman web di URL tersebut.
Kita sudah ngebikin web scraper mini yang bisa jalan sekarang! Silakan pilih beberapa URL lalu jalankan alat command line kita. Kita mungkin mendapati kalau beberapa situs secara konsisten memang lebih kencang dibanding yang lain, sementara di kasus lain situs yang kencang itu berubah-ubah di tiap jalan. Yang lebih penting, kita sudah belajar dasar-dasar dari bekerja dengan futures, jadi sekarang kita bisa gali lebih dalam soal apa yang bisa kita lakukan dengan asinkron.