Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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).

Filename: src/main.rs
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())
}
Listing 17-1: Mendefinisikan fungsi asinkron buat dapat elemen judul dari halaman 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::spawn di “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.

Filename: src/main.rs
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())
}
Listing 17-2: Chaining (menyambung) dengan keyword await

Selesai 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 Trait yang sudah kita bahas dulu di Bab 10 di bagian “Traits sebagai Parameter”.
  • Nilai yang dikembalikan mengimplementasikan trait Future dengan associated type Output. Perhatikan bahwa tipe Output-nya adalah Option<String>, yang mana sama dengan tipe kembalian asli dari versi async fn si page_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 tipe Output di tipe kembaliannya. Ini sama persis kayak blok-blok lain yang pernah kita lihat.
  • Isi fungsi baru tersebut adalah sebuah blok async move gara-gara gimana dia memakai parameter url. (Kita bakal ngebahas lebih banyak lagi soal async versus async move nanti 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.

Filename: src/main.rs
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())
}
Listing 17-3: Memanggil fungsi page_title dari main memakai argumen yang dikasih sama user

Kita 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.

Filename: src/main.rs
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())
}
Listing 17-4: Menunggu sebuah blok asinkron dengan trpl::block_on

Pas 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 main yang asinkron. Macros itu menulis ulang async fn main() { ... } jadi fn main normal, 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 sama trpl::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.

Filename: src/main.rs
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)
}
Listing 17-5: Memanggil page_title buat dua URL untuk melihat mana yang kembali duluan

Kita 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::select dibangun di atas fungsi select yang lebih umum yang didefinisikan di crate futures. Fungsi select milik crate futures bisa melakukan banyak hal yang fungsi trpl::select tidak 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.