move menyelesaikan masalah data masuk ke thread, tetapi perpindahan itu searah dan final: begitu String dipindahkan ke closure, thread tersebut pemiliknya sampai mati. Alur kerja nyata jarang sesederhana itu — pesanan harus bolak-balik antara kasir dan barista sepanjang hari. Yang dibutuhkan adalah jalur yang bisa dipakai berulang selama program berjalan, tanpa memindahkan kepemilikan jalurnya sendiri. Rust menyediakannya sebagai channel: antrean pesan antar thread yang mengalirkan nilai satu arah, dari producer ke consumer. Filosofinya terkenal: jangan berkomunikasi dengan cara berbagi memori; sebaliknya, bagikan memori dengan cara berkomunikasi.

Antrean Pesan dengan Dua Ujung #

mpsc adalah singkatan multi-producer, single-consumer: banyak thread boleh mengirim ke satu channel, tetapi hanya satu thread yang menerima darinya. Pemanggilan mpsc::channel() mengembalikan sepasang ujung dalam satu tuple: tx bertipe Sender<T> — pintu kirim — dan rx bertipe Receiver<T> — pintu terima. Di antara keduanya terbentang antrean FIFO: pesan yang masuk lebih dulu keluar lebih dulu.

Mekanisme keamanannya memakai aturan yang sudah kita kenal: send(nilai) mengambil kepemilikan nilai dan memindahkannya ke dalam antrean, sedangkan recv() mengeluarkannya dan menyerahkan kepemilikan kepada penerima. Tidak ada momen ketika dua thread memegang nilai yang sama secara bersamaan. send segera kembali setelah nilai masuk antrean; recv bersifat blocking — menunggu sampai ada pesan atau sampai channel tertutup. Kedua ujung channel adalah nilai biasa yang bisa dipindahkan lewat closure move: siapa yang memegang tx menjadi pengirim, siapa yang memegang rx menjadi penerima.

Mengirim dan Menerima Nilai #

Pola terkecilnya bisa dilihat tanpa thread tambahan — main berperan sebagai kedua ujungnya:

rust
use std::sync::mpsc;

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

    tx.send(String::from("Pesanan Kopi Gayo")).unwrap();
    println!("Pesan masuk ke antrean");

    let pesan = rx.recv().unwrap();
    println!("Diterima: {}", pesan);
}

Output yang Dihasilkan:

text
Pesan masuk ke antrean
Diterima: Pesanan Kopi Gayo

Mari kita bedah kode di atas:

  1. Baris 4 (let (tx, rx) = mpsc::channel();): tuple diuraikan destructuring menjadi dua variabel. Tipe channel-nya diinferensikan dari pesan pertama yang dikirim — channel String — jadi tx: Sender<String> dan rx: Receiver<String>.
  2. Baris 6 (tx.send(String::from("Pesanan Kopi Gayo")).unwrap();): send memindahkan String dari tangan main ke dalam antrean. Metode ini mengembalikan Result karena akan error bila penerima sudah dibuang.
  3. Baris 7 (println!("Pesan masuk ke antrean");): send tidak menunggu siapa pun membaca pesan; nilai sudah tersimpan aman di antrean, dan baris ini tercetak sebelum penerimaan terjadi.
  4. Baris 9 (let pesan = rx.recv().unwrap();): recv mengambil pesan paling depan dan mengembalikan kepemilikannya sebagai String baru bernama pesan. Panggilan ini blocking — bila antrean kosong, eksekusi berhenti sampai pesan datang.

Kepemilikan Ikut Pindah Lewat send #

Kata “memindahkan” bukan metafora. send benar-benar mengambil alih kepemilikan, sehingga variabel yang sudah dikirim mati di pengirimnya — aturan move semantics yang sama dengan move closure, kini di garis depan antar thread. Kode berikut mencoba memakai kembali pesan yang sudah terkirim:

rust
use std::sync::mpsc;

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

    let pesan = String::from("Pesanan Kopi Toraja");
    tx.send(pesan).unwrap();
    println!("Terkirim: {}", pesan);

    println!("Diterima: {}", rx.recv().unwrap());
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0382]: borrow of moved value: `pesan`
 --> main.rs:8:30
  |
6 |     let pesan = String::from("Pesanan Kopi Toraja");
  |         ----- move occurs because `pesan` has type `String`, which does not implement the `Copy` trait
7 |     tx.send(pesan).unwrap();
  |             ----- value moved here
8 |     println!("Terkirim: {}", pesan);
  |                              ^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
7 |     tx.send(pesan.clone()).unwrap();
  |                  ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

Pesan E0382 mencatat kronologinya baris demi baris: pesan dipindahkan di Baris 7 (value moved here) oleh tx.send, lalu dipinjam lagi di Baris 8 (value borrowed here after move). Saat itu, satu-satunya pemilik sah teks adalah antrean channel. Inilah yang membuat channel aman sejak level tipe: setelah dikirim, pengirim tidak bisa menyentuh nilai lagi, sehingga race condition atas nilai tersebut mustahil terjadi.

Banyak Pesan dan Penutupan Saluran #

Channel satu pesan hampir tak berguna. Pola yang lazim: satu thread producer mengirim rangkaian pesan lalu berhenti; consumer menerimanya satu per satu sampai tahu tidak ada lagi yang akan datang. Bagaimana consumer tahu kapan harus berhenti? Antrean dianggap tertutup hanya setelah semua Sender dijatuhkan. Contoh berikut memakai loop for langsung atas rx — Receiver mengimplementasikan Iterator yang berhenti otomatis saat channel tertutup:

rust
use std::sync::mpsc;
use std::thread;

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

    let pengirim = thread::spawn(move || {
        let menu = vec![
            String::from("Kopi Gayo"),
            String::from("Teh Melati"),
            String::from("Wedang Jahe"),
        ];
        for item in menu {
            tx.send(item).unwrap();
        }
    });

    for item in rx {
        println!("Diterima: {}", item);
    }

    pengirim.join().unwrap();
    println!("Saluran tertutup, semua item diterima");
}

Output yang Dihasilkan:

text
Diterima: Kopi Gayo
Diterima: Teh Melati
Diterima: Wedang Jahe
Saluran tertutup, semua item diterima

Mari kita bedah kode di atas:

  1. Baris 7 (let pengirim = thread::spawn(move || {): tx dipindahkan ke thread producer lewat move. Sejak baris ini, main tidak bisa mengirim lagi — main hanya memegang rx.
  2. Baris 8-12 (let menu = vec![...]): tiga String disiapkan milik producer; tidak ada yang dibagikan ke thread lain.
  3. Baris 13-15 (for item in menu { ... }): setiap item dipindahkan ke antrean — urutan pesan mengikuti urutan pengiriman karena antrean bersifat FIFO.
  4. Baris 16 (});): closure selesai, tx dijatuhkan bersamanya. Penutupan inilah yang menandai akhir aliran pesan.
  5. Baris 18-20 (for item in rx { ... }): setiap iterasi menerima dan mencetak pesan yang tiba. Ketika antrean kosong dan semua sender sudah mati, iterator menghasilkan None dan loop berakhir — tidak perlu pesan penanda khusus seperti “SELESAI”.
  6. Baris 22-23: join memastikan producer rampung, lalu baris penutup tercetak. Urutan tiga pesan selalu sama pada setiap eksekusi karena antrean FIFO memelihara urutan pengiriman.

try_recv: Menerima Tanpa Menunggu #

recv yang blocking kadang terlalu setia menunggu. try_recv memeriksa antrean sekali tanpa menunggu: bila ada pesan, pesan diambil; bila kosong atau channel sudah tertutup, panggilan langsung kembali dengan Err. Berikut ketiga situasinya dalam satu program:

rust
use std::sync::mpsc;

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

    match rx.try_recv() {
        Ok(nilai) => println!("Langsung dapat: {}", nilai),
        Err(_) => println!("Antrean masih kosong"),
    }

    tx.send(15000).unwrap();
    match rx.try_recv() {
        Ok(nilai) => println!("Langsung dapat: {}", nilai),
        Err(_) => println!("Antrean masih kosong"),
    }

    drop(tx);
    println!("Setelah penutupan: {:?}", rx.recv());
}

Output yang Dihasilkan:

text
Antrean masih kosong
Langsung dapat: 15000
Setelah penutupan: Err(RecvError)

Mari kita bedah kode di atas:

  1. Baris 6-9 (match rx.try_recv() { ... }): pemeriksaan pertama terjadi sebelum ada kiriman apa pun, sehingga hasilnya Err — antrean masih kosong (Empty). Program tidak berhenti sekalipun tidak ada pesan.
  2. Baris 11-15: setelah tx.send(15000) pada Baris 11, try_recv kedua langsung menemukan pesan dan mengambil nilainya.
  3. Baris 17 (drop(tx);): sender dijatuhkan lebih awal — channel resmi tertutup meski antrean kosong.
  4. Baris 18 (println!("Setelah penutupan: {:?}", rx.recv());): recv pada channel yang tertutup mengembalikan Err(RecvError) yang dicetak dengan format {:?}. Dua penyebab Err kini lengkap: Empty saat antrean kosong tapi sender masih hidup, dan Disconnected saat semua sender sudah mati — sinyal “tidak akan ada pesan lagi” yang bisa dipakai sebagai kondisi berhenti.

Ringkasan #

Channel mpsc mengalirkan nilai dari Sender ke Receiver lewat antrean FIFO: send memindahkan kepemilikan ke antrean (E0382 menunggu siapa pun yang lupa), recv memblokir sampai pesan tiba, dan try_recv memeriksa tanpa menunggu. Channel tertutup ketika semua sender dijatuhkan; iterator rx dan nilai Disconnected adalah dua cara berhenti yang lazim. Dengan satu pengirim dan satu penerima, kepemilikan tidak pernah berbagi. Berikutnya kita buka pintu bagi banyak pengirim sekaligus lewat tx.clone().