select dan Joining

Future yang berlomba, timeout, dan penggabungan hasil.

Daftar isi

Topik ini membahas alat untuk menyusun beberapa future sekaligus pada satu titik await: tokio::select! untuk memperlombakan cabang-cabang dan mengambil hasil yang paling dahulu, timeout untuk membatasi lama penantian, serta tokio::join! untuk menjalankan beberapa future serentak lalu mengumpulkan seluruh hasilnya. Ketiganya dibangun di atas mekanisme poll yang sama, sehingga perilakunya dapat dijelaskan tanpa konsep baru. Setelah mengikuti topik ini, penantian yang berlomba, terbatas waktu, dan digabung dapat ditulis dengan sadar akan konsekuensi pembatalannya.

select: future yang berlomba#

Makro tokio::select! mempoll seluruh cabangnya secara bergantian dan selesai begitu salah satu cabang menghasilkan Poll::Ready. Cabang-cabang lainnya dijatuhkan pada saat itu juga, sehingga makro ini adalah alat lomba, bukan alat pengumpul. Program berikut memakai fungsi salin yang menunggu selama jeda tertentu lalu menghasilkan string, sehingga pemenang lomba dapat ditebak dari lamanya jeda. Cabang "jaringan" berjeda 10 ms selalu mengalahkan cabang "lokal" yang berjeda 30 ms.

toml Cargo.toml
[package]
name = "async-select"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
rust main.rs
// tidak-dijalankan
use std::time::Duration;

use tokio::time::{sleep, timeout};

async fn salin(sumber: &str, jeda: Duration) -> String {
    sleep(jeda).await;
    format!("data dari {}", sumber)
}

#[tokio::main]
async fn main() {
    let hasil = tokio::select! {
        data = salin("lokal", Duration::from_millis(30)) => data,
        data = salin("jaringan", Duration::from_millis(10)) => data,
    };
    println!("select memilih: {}", hasil);

    match timeout(Duration::from_millis(20), salin("arsip", Duration::from_millis(100))).await {
        Ok(data) => println!("selesai sebelum batas waktu: {}", data),
        Err(_) => println!("timeout: salin dari arsip dibatalkan sebelum selesai"),
    }

    let (kiri, kanan) = tokio::join!(
        salin("kiri", Duration::from_millis(10)),
        salin("kanan", Duration::from_millis(20)),
    );
    println!("join: {} dan {}", kiri, kanan);
}

Berkas main.rs ditandai tidak-dijalankan karena bergantung pada crate tokio, dan eksekusinya berlangsung lewat Cargo pada blok bash di bawahnya, dengan proyek contoh yang disiapkan di /tmp.

bash
# mulai dari proyek baru di /tmp agar pohon konten situs tetap bersih
rm -rf /tmp/async-select
# buat proyek binary baru tanpa repositori git
cargo new /tmp/async-select -q --vcs none
# salin berkas proyek dari bundle topik ini
cp Cargo.toml /tmp/async-select/
cp main.rs /tmp/async-select/src/
# jalankan programnya dengan direktori target di luar proyek
CARGO_TARGET_DIR=/tmp/target-async-select cargo run -q --manifest-path /tmp/async-select/Cargo.toml
text Keluaran
select memilih: data dari jaringan
timeout: salin dari arsip dibatalkan sebelum selesai
join: data dari kiri dan data dari kanan

Pada lomba pertama, future "jaringan" menang sehingga future "lokal" dijatuhkan sebelum jeda 30 ms-nya selesai. Cabang yang kalah tidak diberi kesempatan menyelesaikan pekerjaannya, dan seluruh kemajuan yang sudah terjadi padanya hilang ketika cabang itu dijatuhkan. Pola cabang pada select! memakai bentuk pola = future => hasil: nilai yang dihasilkan future pemenang diikat ke nama data, lalu ekspresi setelah => menghasilkan nilai keseluruhan makro.

Pembatalan dan batas waktu#

Fungsi timeout membungkus sebuah future dengan batas waktu dan pada dasarnya adalah select! antara future itu dan timer. Bila future selesai lebih dahulu, hasilnya kembali dalam Ok, sedangkan bila batas waktu habis lebih dahulu, seluruh future dibatalkan dan error Elapsed kembali dalam Err. Pada contoh, penyalinan berjeda 100 ms kehabisan batas 20 ms sehingga yang tercetak adalah cabang Err. Jeda yang dipakai contoh sengaja berjauhan, yaitu 10 lawan 30 ms dan 20 lawan 100 ms, karena lomba dua jeda yang terlalu berdekatan dapat dimenangkan oleh future mana pun bergantung pada penjadwalan.

Pembatalan bekerja lewat mekanisme drop: select! dan timeout menjatuhkan future yang kalah, dan Drop milik future membersihkan seluruh resource yang dibawanya. Konsekuensinya perlu diperhatikan pada operasi yang kemajuannya tidak dapat dilanjutkan dari titik tengah, misalnya read_exact yang sudah menerima sebagian byte lalu dibatalkan: panggilan berikutnya mengulang dari nol dan byte yang sudah tiba dapat hilang. Operasi seperti read yang mengembalikan jumlah byte yang benar-benar diterima pada tiap panggilan lebih aman dipakai berulang di dalam select!.

join dan kerabatnya#

Makro tokio::join! mengambil kebalikan dari lomba: seluruh future dipoll sampai selesai dan hasilnya dikumpulkan menjadi satu tuple. Future-future itu berjalan serentak karena join! mempoll kembali cabang yang sudah bangun, sehingga total waktunya kira-kira sama dengan cabang terlama, bukan jumlah seluruh jeda. Pada contoh, join! atas dua salin berjeda 10 dan 20 ms menghasilkan kedua string setelah kira-kira 20 ms, dan pencetakan tetap berurutan karena tuple hanya terbentuk setelah seluruh cabang selesai.

Kerabat terdekatnya menangani kegagalan dan jumlah dinamis. Makro try_join! berhenti lebih awal dengan Err begitu satu cabang gagal sehingga cabang lain dibatalkan, dan ini cocok saat seluruh hasil harus berhasil atau tidak sama sekali. Untuk kumpulan task yang jumlahnya baru diketahui saat berjalan, JoinSet mengumpulkan JoinHandle dan menyerahkan hasil satu per satu lewat join_next. Sebagai pembanding tanpa tokio, crate futures menyediakan join dan select dengan perilaku yang serupa.

Kesalahan umum#

Kesalahan pertama adalah mengira cabang yang kalah pada select! tetap menyelesaikan pekerjaannya. Cabang itu dijatuhkan seketika, sehingga penulisan berkas, pengiriman pesan, atau pembacaan yang sedang setengah jalan tidak dilanjutkan. Susun ulang bila seluruh cabang memang harus tuntas, misalnya dengan memindahkan pekerjaan ke task terpisah lewat spawn lalu menggabungkannya dengan join!.

Kesalahan kedua adalah memakai join! padahal hanya hasil yang paling dahulu yang dibutuhkan. join! menunggu semua cabang sehingga program tertahan oleh cabang yang paling lambat, dan satu cabang macet membuat seluruh titik await itu macet. Gunakan select! untuk mengambil pemenang, atau timeout bila yang dibutuhkan adalah batas waktu.

Kesalahan ketiga adalah memperlombakan dua future dengan jeda yang nyaris sama dan mengandalkan urutan keluarannya. Selisih beberapa mikrodetik tidak menjamin pemenang karena penjadwalan dapat menggeser poll satu cabang. Beri jeda yang berjauhan pada contoh dan pengujian, lalu tulis logika yang benar untuk kedua kemungkinan pemenang.

Ringkasan#

  • tokio::select! mempoll seluruh cabang dan selesai bersama pemenang, sementara cabang yang kalah dijatuhkan sehingga kemajuannya hilang.
  • timeout membatasi penantian sebuah future dan mengembalikan Err(Elapsed) bila batas waktu lebih dahulu, dengan pembatalan yang sama seperti select!.
  • tokio::join! menjalankan seluruh cabang serentak dan mengumpulkan hasilnya sebagai tuple, dengan total waktu kira-kira cabang terlama.
  • try_join! membatalkan cabang lain saat satu cabang gagal, dan JoinSet mengelola kumpulan task yang jumlahnya dinamis.

Referensi#