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

Ngelihat Lebih Dekat pada Traits buat Async

Di sepanjang bab ini, kita sudah memakai trait Future, Stream, dan StreamExt dengan berbagai cara. Sejauh ini, kita memang sengaja menghindari membahas terlalu dalam soal gimana detail cara kerja mereka atau gimana mereka saling berhubungan, yang mana sebenarnya sah-sah saja buat pekerjaan Rust kita sehari-hari. Tapi kadang-kadang, kita bakal menjumpai situasi di mana kita butuh memahami lebih banyak detail soal trait-trait ini, bersamaan dengan tipe Pin dan trait Unpin. Di bagian ini, kita bakal menggalinya secukupnya saja buat membantu kita di skenario-skenario tersebut, sembari tetap membiarkan pembahasan yang bener-bener mendalam buat dokumentasi lainnya.

Trait Future

Mari kita mulai dengan melihat lebih dekat gimana cara kerja trait Future. Beginilah cara Rust mendefinisikannya:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Definisi trait tersebut menyertakan sejumlah tipe baru dan juga beberapa sintaks yang belum pernah kita lihat sebelumnya, jadi mari kita bedah definisinya satu per satu.

Pertama, associated type Output milik Future menyatakan hasil akhir yang dihasilkan sama future tersebut. Ini serupa dengan associated type Item pada trait Iterator. Kedua, Future punya method poll, yang menerima sebuah referensi Pin spesial buat parameter self-nya dan sebuah referensi mutable ke tipe Context, serta mengembalikan sebuah Poll<Self::Output>. Kita bakal ngomongin soal Pin dan Context sebentar lagi. Buat sekarang, mari fokus sama apa yang dikembalikan sama method-nya, yaitu tipe Poll:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

Tipe Poll ini mirip sama Option. Ia punya satu varian yang punya nilai, Ready(T), dan satu lagi yang nggak punya, Pending. Tapi makna Poll itu jauh beda lho sama Option! Varian Pending mengindikasikan kalau future-nya masih punya pekerjaan buat dilakukan, jadi si pemanggil perlu mengeceknya lagi nanti. Varian Ready mengindikasikan kalau Future-nya sudah selesai mengerjakan tugasnya dan nilai T sudah tersedia.

Catatan: Jarang sekali ada kebutuhan buat memanggil poll secara langsung, tapi kalau kita memang butuh melakukannya, ingatlah kalau pada kebanyakan futures, si pemanggil tidak seharusnya memanggil poll lagi setelah future-nya mengembalikan Ready. Banyak futures yang bakal panic kalau di-poll lagi setelah mereka jadi siap. Futures yang aman buat di-poll lagi bakal menyatakannya secara eksplisit di dalam dokumentasinya. Perilaku ini mirip sama gimana Iterator::next bekerja.

Pas kita melihat kode yang memakai await, Rust sebenarnya mengompilasi kode tersebut di balik layar menjadi kode yang memanggil poll. Kalau kita melihat balik ke Listing 17-4, di mana kita mencetak judul halaman buat satu URL begitu dia selesai, Rust mengompilasinya jadi sesuatu yang kira-kira (biarpun nggak persis sama) kayak gini:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    },
    Pending => {
        // Terus apa yang ditaruh di sini?
    }
}

Apa yang harus kita lakukan pas future-nya masih Pending? Kita butuh suatu cara buat mencoba lagi, lagi, dan lagi, sampai future-nya akhirnya siap. Dengan kata lain, kita butuh sebuah loop:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
        Pending => {
            // lanjut
        }
    }
}

Tapi kalau seandainya Rust benar-benar mengompilasi kode yang persis kayak gitu, tiap await jadinya bakal memblokir—persis kebalikan dari apa yang mau kita capai! Sebaliknya, Rust memastikan kalau perulangannya bisa menyerahkan kontrol ke sesuatu yang bisa me-pause pekerjaan pada future ini buat mengerjakan futures lainnya dan baru kemudian mengecek yang satu ini lagi nanti. Kayak yang sudah kita lihat, “sesuatu” itu adalah sebuah async runtime, dan pekerjaan penjadwalan dan koordinasi ini adalah salah satu tugas utamanya.

Di bagian “Mengirim Data di Antara Dua Task Memakai Message Passing”, kita mendeskripsikan proses menunggu di rx.recv. Pemanggilan recv mengembalikan sebuah future, dan me-await future tersebut bakal me- poll-nya. Kita sudah mencatat kalau sebuah runtime bakal me-pause future-nya sampai dia siap dengan entah Some(message) atau None pas channel-nya tutup. Dengan pemahaman kita yang lebih dalam soal trait Future, dan secara spesifik Future::poll, kita bisa melihat gimana cara kerjanya. Runtime tahu kalau future-nya belum siap pas dia mengembalikan Poll::Pending. Kebalikannya, runtime tahu kalau future-nya sudah siap dan melanjutkannya pas poll mengembalikan Poll::Ready(Some(message)) atau Poll::Ready(None).

Detail persis soal gimana cara runtime melakukan hal tersebut ada di luar cakupan buku ini, tapi kuncinya adalah melihat mekanisme dasar dari futures: sebuah runtime me-poll tiap future yang jadi tanggung jawabnya, lalu menidurkan kembali future tersebut pas dia belum siap.

Tipe Pin dan Trait Unpin

Dulu di Listing 17-13, kita memakai macro trpl::join! buat menunggu tiga buah futures. Tapi, sudah jadi hal yang umum kalau kita punya sebuah koleksi seperti vector yang berisi sejumlah futures yang jumlahnya baru bakal diketahui pas runtime. Mari kita ubah Listing 17-13 jadi kode di Listing 17- 23 yang menaruh ketiga futures tersebut ke dalam sebuah vector lalu memanggil fungsi trpl::join_all sebagai gantinya, yang mana kode ini belum bisa di- compile.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            // --snip--
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures: Vec<Box<dyn Future<Output = ()>>> =
            vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

        trpl::join_all(futures).await;
    });
}
Listing 17-23: Menunggu futures di dalam sebuah koleksi

Kita menaruh tiap future di dalam sebuah Box buat menjadikannya sebagai trait objects, persis kayak yang kita lakukan di bagian “Mengembalikan Error dari run” di Bab 12. (Kita bakal membahas trait objects secara detail di Bab 18.) Memakai trait objects membiarkan kita memperlakukan tiap futures anonim yang dihasilkan sama tipe-tipe ini sebagai tipe yang sama, karena mereka semua mengimplementasikan trait Future.

Ini mungkin mengejutkan. Lagian, tidak ada satu pun dari blok asinkron tersebut yang mengembalikan apa-apa, jadi masing-masing memproduksi sebuah Future<Output = ()>. Tapi ingat kalau Future itu adalah sebuah trait, dan compiler membikin sebuah enum yang unik buat tiap blok asinkron, biarpun tipe output mereka identik. Sama halnya kayak kita tidak bisa menaruh dua buah struct tulisan tangan yang berbeda ke dalam sebuah Vec, kita juga tidak bisa mencampur aduk enum-enum buatan compiler tersebut.

Terus kita mengoper koleksi futures tersebut ke fungsi trpl::join_all lalu me-await hasilnya. Tapi, kode ini tidak bisa di-compile; ini dia bagian yang relevan dari pesan errornya.

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

Catatan di pesan error ini ngasih tahu kita kalau kita seharusnya memakai macro pin! buat me-pin (mematok) nilai-nilainya, yang artinya menaruh mereka di dalam tipe Pin yang menjamin kalau nilai-nilainya tidak bakal dipindah-pindah di dalam memori. Pesan error-nya bilang kalau proses pinning itu diwajibkan karena dyn Future<Output = ()> perlu mengimplementasikan trait Unpin dan untuk sekarang dia belum melakukannya.

Fungsi trpl::join_all mengembalikan sebuah struct bernama JoinAll. Struct tersebut sifatnya generik terhadap tipe F, yang dibatasi (constrained) buat mengimplementasikan trait Future. Menunggu sebuah future secara langsung dengan await bakal me-pin future-nya secara implisit. Itulah alasan kenapa kita tidak perlu memakai pin! di semua tempat di mana kita mau me- await futures.

Tapi, kita ini sedang tidak menunggu sebuah future secara langsung di sini. Sebaliknya, kita sedang membangun sebuah future baru, JoinAll, dengan cara meneruskan sekumpulan futures ke fungsi join_all. Signature buat join_all mewajibkan tipe-tipe dari item di dalam koleksinya buat semuanya mengimplementasikan trait Future, dan Box<T> mengimplementasikan Future cuma kalau T yang ia bungkus itu adalah sebuah future yang mengimplementasikan trait Unpin.

Duh, sangat banyak yang harus diserap ya! Biar benar-benar paham, mari kita gali sedikit lebih jauh soal gimana cara trait Future sebenarnya bekerja, terutama seputar urusan pinning. Mari kita lihat lagi definisi dari trait Future:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // Method yang wajib ada
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Parameter cx dan tipe Context-nya adalah kunci gimana sebuah runtime bisa benar-benar tahu kapan harus mengecek sembarang future yang diberikan sambil tetap bersifat malas. Sekali lagi, detail gimana itu bekerja ada di luar cakupan bab ini, dan kita umumnya cuma perlu memikirkan hal ini pas lagi menulis implementasi Future kustom. Kita bakal fokus saja ke tipe buat self, karena ini adalah pertama kalinya kita melihat sebuah method di mana self punya anotasi tipe. Sebuah anotasi tipe buat self bekerja kayak anotasi tipe buat parameter fungsi lainnya tapi dengan dua perbedaan kunci:

  • Ia memberi tahu Rust tipe self apa yang harus dimiliki supaya method-nya bisa dipanggil.
  • Ia nggak boleh sembarang tipe. Ia dibatasi cuma buat tipe tempat method-nya diimplementasikan, sebuah referensi atau smart pointer ke tipe tersebut, atau sebuah Pin yang membungkus referensi ke tipe tersebut.

Kita bakal melihat lebih banyak lagi soal sintaks ini di Bab 18. Buat sekarang, cukup tahu saja kalau kita mau me-poll sebuah future buat mengecek apakah dia itu Pending atau Ready(Output), kita butuh referensi mutable yang dibungkus Pin ke tipe tersebut.

Pin adalah sebuah pembungkus (wrapper) buat tipe-tipe yang mirip pointer kayak &, &mut, Box, dan Rc. (Secara teknis, Pin bekerja dengan tipe-tipe yang mengimplementasikan trait Deref atau DerefMut, tapi ini secara efektif ekuivalen dengan bekerja cuma bareng referensi dan smart pointers.) Pin bukanlah sebuah pointer itu sendiri dan ia tidak punya perilaku miliknya sendiri kayak Rc dan Arc yang punya fitur reference counting; ia murni hanyalah alat yang bisa dipakai compiler buat menegakkan batasan (constraints) pada penggunaan pointer.

Mengingat kembali kalau await itu diimplementasikan lewat pemanggilan- pemanggilan ke poll mulai bisa menjelaskan pesan error yang kita lihat tadi, tapi kan tadi itu dalam konteks Unpin, bukan Pin. Jadi apa sebenarnya hubungan Pin dengan Unpin, dan kenapa Future butuh self buat berada di dalam tipe Pin supaya bisa memanggil poll?

Ingat dari bagian awal bab ini kalau serangkaian titik await di dalam sebuah future bakal dikompilasi jadi sebuah state machine, dan compiler mastiin kalau state machine tersebut mengikuti semua aturan normal Rust seputar keamanan, termasuk borrowing dan ownership. Biar itu bisa jalan, Rust melihat data apa saja yang dibutuhkan di antara satu titik await dengan titik await berikutnya atau sampai akhir dari blok asinkronnya. Terus dia membikin varian yang korespondensi di dalam state machine hasil kompilasinya. Tiap varian dapet akses yang dibutuhkannya ke data yang bakal dipakai di bagian kode sumber tersebut, entah dengan mengambil kepemilikan dari data tersebut atau dengan mendapatkan referensi mutable atau immutable kepadanya.

Sejauh ini aman: kalau kita bikin kesalahan soal ownership atau referensi di sebuah blok asinkron tertentu, si borrow checker bakal kasih tahu kita. Tapi pas kita mau memindah-mindahkan future yang berkaitan sama blok tersebut—kayak memindahkannya ke dalam sebuah Vec buat dioper ke join_all—urusannya jadi makin rumit.

Pas kita memindahkan sebuah future—entah itu dengan memasukkannya ke struktur data buat dipakai sebagai iterator bersama join_all atau dengan mengembalikannya dari sebuah fungsi—itu sebenarnya bermakna memindahkan state machine yang Rust bikin buat kita. Dan beda sama mayoritas tipe lain di Rust, futures yang Rust bikin buat blok asinkron bisa berakhir punya referensi ke dirinya sendiri di dalam field dari varian yang manapun, kayak yang ditunjukkan di ilustrasi yang disederhanakan di Gambar 17-4.

Sebuah tabel dengan satu kolom dan tiga baris yang merepresentasikan sebuah future, fut1, yang punya nilai data 0 dan 1 di dua baris pertamanya dan sebuah panah yang menunjuk dari baris ketiga balik ke baris kedua, melambangkan sebuah referensi internal di dalam future tersebut.
Gambar 17-4: Sebuah tipe data yang punya referensi ke dirinya sendiri (self-referential)

Tapi secara bawaan, objek apa saja yang punya referensi ke dirinya sendiri itu sifatnya tidak aman buat dipindahkan, karena referensi itu selalu menunjuk ke alamat memori asli dari apa pun yang dirujuknya (lihat Gambar 17-5). Kalau kita memindahkan struktur datanya itu sendiri, referensi internal tadi bakal ditinggalkan dalam posisi menunjuk ke lokasi yang lama. Padahal, lokasi memori tersebut sekarang sudah tidak valid. Di satu sisi, nilainya tidak bakal di- update pas kita melakukan perubahan ke struktur datanya. Di sisi lain—yang lebih penting—komputernya sekarang bebas buat memakai ulang memori tersebut buat keperluan lain! Kita bisa berakhir membaca data yang sama sekali tidak ada hubungannya nanti.

Dua tabel, menggambarkan dua futures, fut1 dan fut2, yang masing-masing punya satu kolom dan tiga baris, merepresentasikan hasil dari memindahkan sebuah future keluar dari fut1 ke fut2. Yang pertama, fut1, digelapkan warnanya, dengan tanda tanya di tiap indeksnya, melambangkan memori yang tidak diketahui. Yang kedua, fut2, punya 0 dan 1 di baris pertama dan kedua dan sebuah panah yang menunjuk dari baris ketiganya balik ke baris kedua milik fut1, melambangkan sebuah pointer yang sedang merujuk ke lokasi lama di memori milik future tersebut sebelum ia dipindahkan.
Gambar 17-5: Hasil yang tidak aman dari memindahkan tipe data yang bersifat self-referential

Secara teori, compiler Rust bisa saja mencoba buat meng-update tiap referensi ke suatu objek setiap kali objek tersebut dipindahkan, tapi itu bisa menambah banyak beban performa, apalagi kalau ada jaring-jaring referensi utuh yang perlu di-update. Kalau kita sebaliknya bisa memastikan struktur data yang dimaksud itu tidak pindah-pindah di memori, kita tidak perlu repot-repot meng- update referensi apa pun. Nah, itulah gunanya borrow checker milik Rust: di dalam kode yang aman, ia mencegah kita dari memindahkan item apa saja yang punya referensi aktif yang menunjuk kepadanya.

Pin dibangun di atas hal tersebut buat memberikan jaminan persis yang kita butuhkan. Pas kita me-pin sebuah nilai dengan membungkus sebuah pointer ke nilai tersebut di dalam Pin, dia sudah tidak bisa pindah lagi. Jadi, kalau kita punya Pin<Box<SuatuTipe>>, kita sebenarnya sedang me-pin nilai SuatuTipe tersebut, bukan pointer Box-nya. Gambar 17-6 mengilustrasikan proses ini.

Tiga kotak diletakkan berdampingan. Yang pertama dilabeli “Pin”, yang kedua “b1”, dan yang ketiga “pinned”. Di dalam “pinned” ada tabel dilabeli “fut”, dengan satu kolom; ia merepresentasikan sebuah future dengan sel-sel buat tiap bagian dari struktur datanya. Sel pertamanya punya nilai “0”, sel keduanya punya panah yang keluar darinya dan menunjuk ke sel keempat dan terakhir, yang punya nilai “1” di dalamnya, dan sel ketiga punya garis putus-putus dan elipsis buat menandakan mungkin ada bagian lain dari struktur datanya. Secara keseluruhan, tabel “fut” merepresentasikan sebuah future yang bersifat self-referential. Sebuah panah keluar dari kotak berlabel “Pin”, melewati kotak berlabel “b1” dan berakhir di dalam kotak “pinned” di tabel “fut”.
Gambar 17-6: Me-pin sebuah `Box` yang menunjuk ke tipe future yang bersifat self-referential

Kenyataannya, pointer Box-nya tetap bisa pindah-pindah dengan bebas. Ingat: kita peduli buat memastikan kalau data yang ujung-ujungnya sedang direferensikan itu tetap diam di tempat. Kalau sebuah pointer pindah-pindah, tapi data yang ditunjuknya tetap ada di tempat yang sama, kayak di Gambar 17-7, nggak bakal ada masalah potensial. (Sebagai latihan mandiri, coba lihat dokumentasi buat tipe-tipe tersebut sekaligus modul std::pin dan coba cari tahu gimana cara kita melakukan ini pakai Pin yang membungkus sebuah Box.) Kuncinya adalah tipe self-referential-nya itu sendiri nggak bisa pindah, karena ia tetap di- pin.

Empat kotak diletakkan di tiga kolom kasar, identik dengan diagram sebelumnya dengan perubahan di kolom kedua. Sekarang ada dua kotak di kolom kedua, berlabel “b1” dan “b2”, “b1” warnanya digelapkan, dan panah dari “Pin” melewati “b2” bukannya “b1”, menandakan kalau pointernya sudah pindah dari “b1” ke “b2”, tapi data di dalam “pinned” belum pindah.
Gambar 17-7: Memindahkan sebuah `Box` yang menunjuk ke tipe future yang bersifat self-referential

Meskipun begitu, mayoritas tipe itu benar-benar aman buat dipindah-pindahkan, biarpun mereka kebetulan ada di balik sebuah pointer Pin. Kita cuma perlu mikirin soal pinning pas item-itemnya punya referensi internal. Nilai-nilai primitif kayak angka dan Boolean itu aman karena mereka jelas nggak punya referensi internal apa-apa. Begitu juga sama mayoritas tipe yang biasa kita pakai di Rust. Kita bisa memindah-mindahkan sebuah Vec, misalnya, tanpa perlu khawatir. Mengingat apa yang sudah kita lihat sejauh ini, kalau kita punya Pin<Vec<String>>, kita bakal dipaksa melakukan segalanya lewat API milik Pin yang aman tapi membatasi, padahal sebuah Vec<String> itu selalu aman buat dipindahkan kalau tidak ada referensi lain kepadanya. Kita butuh sebuah cara buat memberi tahu compiler kalau tidak apa-apa buat memindah-mindahkan item di kasus-kasus kayak gini—dan di situlah Unpin beraksi.

Unpin adalah sebuah marker trait, mirip sama trait Send dan Sync yang kita lihat di Bab 16, dan oleh karenanya tidak punya fungsionalitas miliknya sendiri. Marker traits eksis cuma buat memberi tahu compiler kalau tipe yang mengimplementasikan trait tersebut aman buat dipakai di suatu konteks tertentu. Unpin menginformasikan ke compiler kalau suatu tipe tertentu tidak perlu menjunjung tinggi jaminan apa pun soal apakah nilai yang dimaksud bisa dipindahkan dengan aman atau tidak.

Sama kayak Send dan Sync, compiler mengimplementasikan Unpin secara otomatis buat semua tipe di mana dia bisa membuktikan keamanannya. Kasus spesialnya, sekali lagi mirip kayak Send dan Sync, adalah pas Unpin tidak diimplementasikan buat suatu tipe. Notasi buat hal ini adalah impl !Unpin for SuatuTipe, di mana SuatuTipe adalah nama dari tipe yang memang perlu menjunjung tinggi jaminan-jaminan tersebut supaya aman kapan pun sebuah pointer ke tipe tersebut dipakai di dalam sebuah Pin.

Dengan kata lain, ada dua hal yang harus diingat soal hubungan antara Pin dan Unpin. Pertama, Unpin adalah kasus yang “normal”, dan !Unpin adalah kasus yang spesial. Kedua, apakah suatu tipe mengimplementasikan Unpin atau !Unpin itu hanya berpengaruh pas kita lagi memakai pointer yang di-pin ke tipe tersebut kayak Pin<&mut SuatuTipe>.

Buat menjadikannya konkret, coba pikirkan soal sebuah String: ia punya sebuah panjang (length) dan karakter-karakter Unicode yang menyusunnya. Kita bisa membungkus sebuah String di dalam Pin, kayak yang terlihat di Gambar 17-8. Tapi, String secara otomatis mengimplementasikan Unpin, sama halnya kayak mayoritas tipe lain di Rust.

Sebuah kotak berlabel “Pin” di sebelah kiri dengan sebuah panah yang mengarah darinya ke sebuah kotak berlabel “String” di sebelah kanan. Kotak “String” berisi data 5usize, melambangkan panjang dari string-nya, dan huruf-huruf “h”, “e”, “l”, “l”, dan “o” melambangkan karakter dari string “hello” yang disimpan di dalam instance String ini. Sebuah persegi panjang putus-putus mengelilingi kotak “String” dan labelnya, tapi tidak dengan kotak “Pin”.
Gambar 17-8: Me-pin sebuah `String`; garis putus-putusnya menandakan kalau `String` tersebut mengimplementasikan trait `Unpin` dan oleh karena itu tidak benar-benar ter-pin

Hasilnya, kita bisa melakukan hal-hal yang tadinya ilegal kalau seandainya String mengimplementasikan !Unpin, kayak misalnya mengganti satu string dengan yang lain di lokasi memori yang sama persis kayak di Gambar 17-9. Ini tidak melanggar kontrak dari Pin, karena String nggak punya referensi internal yang bikin dia jadi nggak aman buat dipindah-pindahkan. Itulah persisnya kenapa ia mengimplementasikan Unpin bukannya !Unpin.

Data string “hello” yang sama dari contoh sebelumnya, sekarang dilabeli “s1” dan warnanya digelapkan. Kotak “Pin” dari contoh sebelumnya sekarang menunjuk ke instance String yang berbeda, yang dilabeli “s2”, bersifat valid, punya panjang 7usize, dan berisi karakter-karakter dari string “goodbye”. s2 juga dikelilingi sama persegi panjang putus-putus karena ia juga mengimplementasikan trait Unpin.
Gambar 17-9: Mengganti si `String` dengan `String` yang benar-benar berbeda di memori

Nah sekarang kita sudah tahu cukup banyak buat memahami error-error yang dilaporkan buat pemanggilan join_all tadi balik di Listing 17-23. Kita awalnya mencoba memindahkan futures hasil produksi blok asinkron ke dalam sebuah Vec<Box<dyn Future<Output = ()>>>, tapi kayak yang sudah kita lihat, futures tersebut mungkin saja punya referensi internal, jadi mereka tidak secara otomatis mengimplementasikan Unpin. Begitu kita me-pin mereka, kita bisa meneruskan tipe Pin hasilnya ke dalam Vec, dengan rasa percaya diri kalau data yang mendasari futures tersebut tidak bakal dipindahkan. Listing 17-24 menunjukkan cara membetulkan kodenya dengan memanggil macro pin! di tiap tempat di mana ketiga futures tadi didefinisikan dan menyesuaikan tipe dari trait object-nya.

extern crate trpl; // required for mdbook test

use std::pin::{Pin, pin};

// --snip--

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let rx_fut = pin!(async {
            // --snip--
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        });

        let tx_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
            vec![tx1_fut, rx_fut, tx_fut];

        trpl::join_all(futures).await;
    });
}
Listing 17-24: Me-pin futures buat memungkinkan mereka dipindahkan ke dalam vector

Contoh ini sekarang sudah bisa di-compile dan dijalankan, dan kita bisa menambah atau menghapus futures dari vector-nya pas runtime lalu menggabungkan mereka semua.

Pin dan Unpin itu paling banyak kepake pas lagi membangun lower-level libraries, atau pas kita lagi membangun sebuah runtime itu sendiri, bukannya buat kode Rust sehari-hari. Tapi pas kita melihat trait-trait ini muncul di dalam pesan error, setidaknya sekarang kita punya gambaran yang lebih oke soal gimana cara membetulkan kode kita!

Catatan: Kombinasi antara Pin dan Unpin ini memungkinkan implementasi aman dari keseluruhan kelas tipe-tipe kompleks di Rust yang tadinya bakal terbukti menantang gara-gara mereka bersifat self-referential. Tipe-tipe yang mewajibkan Pin paling sering muncul di Rust asinkron saat ini, tapi sekali-sekali, kita mungkin juga bakal menjumpai mereka di konteks lain.

Detail spesifik soal gimana Pin dan Unpin bekerja, dan aturan apa saja yang wajib mereka junjung tinggi, sudah dibahas secara ekstensif di dalam dokumentasi API buat std::pin, jadi kalau kita tertarik buat belajar lebih lanjut, itu adalah tempat yang bagus buat memulai.

Kalau kita mau memahami gimana cara kerja di balik layarnya secara lebih detail lagi, silakan baca Bab 2 dan 4 dari buku Asynchronous Programming in Rust.

Trait Stream

Sekarang setelah kita punya pemahaman yang lebih dalam soal trait Future, Pin, dan Unpin, kita bisa mengalihkan perhatian kita ke trait Stream. Kayak yang sudah kita pelajari sebelumnya di bab ini, streams itu mirip kayak iterator asinkron. Tapi beda sama Iterator dan Future, Stream itu tidak punya definisi di dalam standard library pada saat tulisan ini dibuat, tapi emang ada definisi yang sangat umum dari crate futures yang dipakai di seluruh ekosistem.

Mari kita ulas balik definisi dari trait Iterator dan Future sebelum melihat gimana sebuah trait Stream mungkin bakal menggabungkan keduanya. Dari Iterator, kita dapet ide soal urutan (sequence): method next-nya menyediakan sebuah Option<Self::Item>. Dari Future, kita dapet ide soal kesiapan seiring berjalannya waktu: method poll-nya menyediakan sebuah Poll<Self::Output>. Buat merepresentasikan serangkaian item yang mulai siap seiring waktu, kita mendefinisikan sebuah trait Stream yang menggabungkan fitur-fitur tersebut:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Trait Stream mendefinisikan sebuah associated type bernama Item buat tipe dari item-item yang diproduksi sama stream-nya. Ini mirip sama Iterator, di mana itemnya bisa berjumlah nol sampai banyak, dan beda sama Future, yang mana output-nya selalu satu, biarpun itu cuma tipe unit ().

Stream juga mendefinisikan sebuah method buat mendapatkan item-item tersebut. Kita menamainya poll_next, buat memperjelas kalau dia me-poll pakai cara yang sama kayak yang dilakukan Future::poll dan memproduksi serangkaian item pakai cara yang sama kayak yang dilakukan Iterator::next. Tipe kembaliannya menggabungkan Poll dengan Option. Tipe luarnya adalah Poll, karena dia wajib dicek kesiapannya, persis kayak sebuah future. Tipe dalamnya adalah Option, karena dia butuh memberi tanda apakah masih ada pesan lagi, persis kayak sebuah iterator.

Sesuatu yang sangat mirip dengan definisi ini kemungkinan besar bakal berakhir menjadi bagian dari standard library milik Rust. Sembari menunggu, ia adalah bagian dari peralatan milik mayoritas runtimes, jadi kita bisa mengandalkannya, dan segala hal yang kita bahas selanjutnya secara umum bakal tetap berlaku!

Tapi di contoh-contoh yang kita lihat di bagian “Streams: Futures dalam Urutan” tadi, kita tidak memakai poll_next maupun Stream, melainkan malah memakai next dan StreamExt. Tentu saja kita bisa bekerja secara langsung mengikuti API poll_next dengan cara menulis tangan state machine Stream kita sendiri, persis kayak kita juga bisa bekerja bareng futures secara langsung lewat method poll-nya. Tapi memakai await itu jauh lebih enak, dan trait StreamExt menyuplai method next supaya kita bisa melakukan hal itu:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // other methods...
}
}

Catatan: Definisi asli yang kita pakai sebelumnya di bab ini kelihatan sedikit berbeda dari ini, karena ia mendukung versi Rust yang dulu belum mendukung penggunaan fungsi asinkron di dalam traits. Hasilnya, ia kelihatannya kayak gini:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

Tipe Next tersebut adalah sebuah struct yang mengimplementasikan Future dan mengizinkan kita buat menamai lifetime dari referensi ke self dengan Next<'_, Self>, supaya await bisa bekerja bareng method ini.

Trait StreamExt juga merupakan rumah dari semua method menarik yang tersedia buat dipakai bareng streams. StreamExt secara otomatis diimplementasikan buat tiap tipe yang mengimplementasikan Stream, tapi trait-trait ini didefinisikan secara terpisah supaya komunitas bisa beriterasi pada API-API kemudahan tanpa mengganggu trait dasarnya.

Di versi StreamExt yang dipakai di crate trpl, trait-nya tidak cuma mendefinisikan method next tapi juga menyuplai implementasi default dari next yang secara benar menangani detail-detail pemanggilan Stream::poll_next. Ini artinya bahkan pas kita perlu menulis tipe data streaming kita sendiri, kita cuma harus mengimplementasikan Stream, dan nantinya siapa saja yang memakai tipe data kita bisa memakai StreamExt beserta method- methodnya secara otomatis.

Nah, segitu saja pembahasan kita soal detail-detail tingkat rendah pada trait- trait ini. Sebagai penutup, mari kita pertimbangkan gimana futures (termasuk streams), tasks, dan threads semuanya saling melengkapi!