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:
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:
Pesan masuk ke antrean
Diterima: Pesanan Kopi GayoMari kita bedah kode di atas:
- Baris 4 (
let (tx, rx) = mpsc::channel();): tuple diuraikan destructuring menjadi dua variabel. Tipe channel-nya diinferensikan dari pesan pertama yang dikirim — channelString— jaditx: Sender<String>danrx: Receiver<String>. - Baris 6 (
tx.send(String::from("Pesanan Kopi Gayo")).unwrap();):sendmemindahkanStringdari tanganmainke dalam antrean. Metode ini mengembalikanResultkarena akan error bila penerima sudah dibuang. - Baris 7 (
println!("Pesan masuk ke antrean");):sendtidak menunggu siapa pun membaca pesan; nilai sudah tersimpan aman di antrean, dan baris ini tercetak sebelum penerimaan terjadi. - Baris 9 (
let pesan = rx.recv().unwrap();):recvmengambil pesan paling depan dan mengembalikan kepemilikannya sebagaiStringbaru bernamapesan. 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:
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:
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:
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:
Diterima: Kopi Gayo
Diterima: Teh Melati
Diterima: Wedang Jahe
Saluran tertutup, semua item diterimaMari kita bedah kode di atas:
- Baris 7 (
let pengirim = thread::spawn(move || {):txdipindahkan ke thread producer lewatmove. Sejak baris ini,maintidak bisa mengirim lagi —mainhanya memegangrx. - Baris 8-12 (
let menu = vec![...]): tigaStringdisiapkan milik producer; tidak ada yang dibagikan ke thread lain. - Baris 13-15 (
for item in menu { ... }): setiapitemdipindahkan ke antrean — urutan pesan mengikuti urutan pengiriman karena antrean bersifat FIFO. - Baris 16 (
});): closure selesai,txdijatuhkan bersamanya. Penutupan inilah yang menandai akhir aliran pesan. - Baris 18-20 (
for item in rx { ... }): setiap iterasi menerima dan mencetak pesan yang tiba. Ketika antrean kosong dan semua sender sudah mati, iterator menghasilkanNonedan loop berakhir — tidak perlu pesan penanda khusus seperti “SELESAI”. - Baris 22-23:
joinmemastikan 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:
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:
Antrean masih kosong
Langsung dapat: 15000
Setelah penutupan: Err(RecvError)Mari kita bedah kode di atas:
- Baris 6-9 (
match rx.try_recv() { ... }): pemeriksaan pertama terjadi sebelum ada kiriman apa pun, sehingga hasilnyaErr— antrean masih kosong (Empty). Program tidak berhenti sekalipun tidak ada pesan. - Baris 11-15: setelah
tx.send(15000)pada Baris 11,try_recvkedua langsung menemukan pesan dan mengambil nilainya. - Baris 17 (
drop(tx);): sender dijatuhkan lebih awal — channel resmi tertutup meski antrean kosong. - Baris 18 (
println!("Setelah penutupan: {:?}", rx.recv());):recvpada channel yang tertutup mengembalikanErr(RecvError)yang dicetak dengan format{:?}. Dua penyebabErrkini 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().