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

Menyatukan Semuanya: Futures, Tasks, dan Threads

Seperti yang sudah kita lihat di Bab 16, threads menyediakan salah satu pendekatan buat konkurensi. Kita juga sudah melihat pendekatan lainnya di bab ini: memakai asinkron dengan futures dan streams. Kalau kita bertanya-tanya kapan harus memilih salah satu metode di atas metode lainnya, jawabannya adalah: tergantung! Dan di banyak kasus, pilihannya bukan antara threads atau asinkron, melainkan threads dan asinkron.

Banyak sistem operasi sudah menyuplai model konkurensi berbasis threading selama puluhan tahun, dan banyak bahasa pemrograman yang jadi menyokongnya sebagai hasilnya. Namun, model-model ini bukannya tanpa pertukaran (tradeoffs). Di banyak sistem operasi, tiap thread memakan memori dalam jumlah yang cukup besar. Threads juga cuma jadi opsi pas sistem operasi dan perangkat keras kita memang menyokongnya. Beda sama komputer desktop dan ponsel arus utama, beberapa sistem embedded bahkan tidak punya sistem operasi sama sekali, jadinya mereka juga tidak punya threads.

Model asinkron menyediakan kumpulan tradeoffs yang berbeda—dan pada akhirnya saling melengkapi. Di dalam model asinkron, operasi konkuren tidak mewajibkan adanya thread masing-masing. Sebagai gantinya, mereka bisa berjalan di atas tasks (tugas), kayak pas kita memakai trpl::spawn_task buat memulai pekerjaan dari sebuah fungsi sinkron di bagian streams. Sebuah task itu mirip sama thread, tapi bukannya dikelola oleh sistem operasi, ia dikelola sama kode di tingkat library: yaitu runtime.

Ada alasannya kenapa API buat menelurkan threads dan menelurkan tasks itu sangat mirip. Threads bertindak sebagai batas (boundary) buat sekumpulan operasi sinkron; konkurensi dimungkinkan di antara threads. Tasks bertindak sebagai batas buat sekumpulan operasi asinkron; konkurensi dimungkinkan baik di antara maupun di dalam tasks, karena sebuah task bisa berganti-ganti di antara futures di dalam isinya. Terakhir, futures adalah unit konkurensi Rust yang paling spesifik (granular), dan tiap future bisa merepresentasikan sebuah pohon berisi futures lainnya. Runtime—lebih spesifiknya, executor-nya—mengelola tasks, dan tasks mengelola futures. Dalam hal tersebut, tasks itu mirip kayak threads yang ringan dan dikelola sama runtime dengan tambahan kapabilitas yang datang dari fakta bahwa ia dikelola sama runtime bukannya sama sistem operasi.

Ini tidak berarti kalau async tasks itu selalu lebih baik dibanding threads (atau sebaliknya). Konkurensi memakai threads dalam beberapa hal merupakan model pemrograman yang lebih simpel dibanding konkurensi memakai async. Itu bisa jadi kekuatan atau kelemahan. Threads itu semacam “nyalakan dan lupakan” (fire and forget); mereka nggak punya padanan asli terhadap future, jadi mereka sekadar jalan sampai selesai tanpa bisa diinterupsi kecuali oleh sistem operasinya sendiri.

Dan ternyata threads dan tasks itu sering kali bekerja barengan dengan sangat baik, karena tasks (setidaknya di beberapa runtimes) bisa dipindah- pindahkan antar threads. Bahkan, di balik layar, runtime yang sudah kita pakai—termasuk fungsi spawn_blocking dan spawn_task—sifatnya adalah multi- threaded secara bawaan! Banyak runtimes memakai pendekatan bernama work stealing buat memindah-mindahkan tasks antar threads secara transparan, berdasarkan gimana tiap thread tersebut sedang dimanfaatkan saat itu, demi meningkatkan performa keseluruhan sistem. Pendekatan tersebut sebenarnya mewajibkan adanya threads dan tasks, dan oleh karenanya juga futures.

Pas memikirkan metode mana yang mau dipakai kapan, coba pertimbangkan aturan praktis (rules of thumb) berikut:

  • Kalau pekerjaannya sangat bisa diparalelkan (yaitu, CPU-bound), kayak memproses sekumpulan data di mana tiap bagiannya bisa diproses secara terpisah, threads adalah pilihan yang lebih oke.
  • Kalau pekerjaannya sangat konkuren (yaitu, I/O-bound), kayak menangani pesan-pesan dari sekumpulan sumber berbeda yang mungkin datang di interval atau kecepatan yang berbeda-beda, asinkron adalah pilihan yang lebih oke.

Dan kalau kita butuh baik paralelisme maupun konkurensi, kita tidak harus memilih antara threads dan asinkron. Kita bisa memakai keduanya bersamaan dengan bebas, membiarkan masing-masing memainkan peran yang paling dikuasainya. Misalnya, Listing 17-25 menunjukkan contoh yang lumayan umum dari campuran kayak gini di kode Rust dunia nyata.

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

use std::{thread, time::Duration};

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

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::block_on(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}
Listing 17-25: Mengirim pesan pakai kode memblokir (blocking) di dalam sebuah thread dan menunggu pesan-pesan tersebut di dalam blok asinkron

Kita mulai dengan membikin asinkron channel, terus menelurkan sebuah thread yang mengambil kepemilikan dari sisi pengirim channel tersebut memakai keyword move. Di dalam thread tersebut, kita mengirim angka 1 sampai 10, sambil tidur selama satu detik di tiap angkanya. Terakhir, kita menjalankan sebuah future yang dibuat pakai blok asinkron yang dioper ke trpl::block_on persis kayak apa yang sudah kita lakukan di sepanjang bab ini. Di dalam future tersebut, kita me-await pesan-pesan tadi, persis kayak di contoh-contoh message-passing lainnya yang sudah kita lihat.

Buat balik lagi ke skenario yang kita buka di awal bab, bayangkan menjalankan sekumpulan tugas video encoding memakai thread khusus (karena video encoding itu sifatnya compute-bound) tapi memberikan notifikasi ke UI kalau operasi tersebut sudah selesai memakai asinkron channel. Ada banyak sekali contoh dari jenis kombinasi kayak gini di kasus penggunaan dunia nyata.

Ringkasan

Ini bukan kali terakhir kita melihat soal konkurensi di buku ini. Proyek di Bab 21 bakal menerapkan konsep-konsep ini di situasi yang lebih realistis dibanding contoh-contoh simpel yang dibahas di sini dan membandingkan penyelesaian masalah memakai threading versus tasks dan futures secara lebih langsung.

Metode mana pun yang kita pilih, Rust memberi kita alat-alat yang kita butuhkan buat menulis kode konkuren yang aman dan kencang—baik itu buat web server yang high-throughput maupun buat sistem operasi embedded.

Berikutnya, kita bakal ngomongin soal cara yang idiomatik buat memodelkan masalah dan menstrukturkan solusi seiring dengan semakin besarnya program Rust kita. Selain itu, kita bakal membahas gimana kaitan antara idiom milik Rust dengan idiom yang mungkin kita sudah familier di pemrograman berorientasi objek (object-oriented programming).