Episode ini membedah model async await dan runtime tokio, spawning task serta channel dengan tokio::sync, thread safety melalui trait Send dan Sync, dan pola actor-like untuk mengelola state yang dibagi antar task.

Server di episode 10 sudah bisa menangani banyak koneksi, tetapi kita belum membedah bagaimana itu mungkin. Jawabannya ada di episode 12: concurrency. Rust memiliki salah satu model concurrency paling aman di dunia — keamanannya dijamin oleh sistem tipe, bukan sekadar konvensi.
Episode ini membedah async/await dengan runtime tokio, pembuatan task, komunikasi antar task dengan channel, thread safety lewat trait Send dan Sync, serta pola actor-like untuk state yang dibagi. Setelah episode ini, kalian bisa memproses banyak pekerjaan bersamaan tanpa data race.
Operasi seperti menunggu jaringan atau disk tidak membutuhkan CPU. Async memungkinkan satu thread menangani ribuan operasi yang sedang menunggu: saat satu task menunggu, task lain menggunakan CPU. Tokio adalah runtime yang menjalankan model ini; tambahkan dengan cargo add tokio --features full.
cat > src/main.rs <<'EOF'
async fn kerja_panjang(n: u64) -> u64 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
n * n
}
#[tokio::main]
async fn main() {
let mulai = std::time::Instant::now();
let a = tokio::spawn(kerja_panjang(2));
let b = tokio::spawn(kerja_panjang(3));
println!("hasil: {} dan {}", a.await.unwrap(), b.await.unwrap());
println!("waktu: {:?}", mulai.elapsed());
}
EOF
cargo runtokio::spawn menjalankan task secara bersamaan. Dua task yang masing-masing tidur 100 ms selesai dalam ~100 ms, bukan 200 ms — karena sleep tidak memblokir thread. .await menunggu hasil task tanpa menghentikan runtime. Task tokio dijadwalkan oleh executor multi-thread, sehingga ribuan task bisa hidup dalam satu proses, dan JoinHandle dari spawn membawa Result — error di task tidak menjatuhkan program secara diam-diam.
Channel menghubungkan producer dan consumer. mpsc (multi-producer single-consumer) adalah yang paling umum:
cat > src/main.rs <<'EOF'
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(16);
for i in 0..3 {
let tx = tx.clone();
tokio::spawn(async move {
let _ = tx.send(format!("pesan {}", i)).await;
});
}
drop(tx);
while let Some(pesan) = rx.recv().await {
println!("terima: {}", pesan);
}
}
EOF
cargo runmpsc::channel(16) membuat channel dengan buffer 16. Setiap producer meng-clone pengirimnya, dan rx.recv().await menunggu pesan berikutnya. drop(tx) menutup channel sehingga loop berakhir. Pattern ini menyalurkan hasil dari banyak task ke satu consumer.
Selain mpsc, tokio menyediakan watch untuk state yang diikuti banyak task dan broadcast untuk mengirim ke semua subscriber. tokio::sync::watch dipakai misalnya untuk menyebarkan status konfigurasi yang berubah, sedangkan broadcast untuk event ke banyak penerima.
Dua trait menentukan apakah nilai aman dipindah atau dibagi antar thread:
Send: nilai aman dipindahkan ke thread lain.Sync: nilai aman direferensikan dari banyak thread sekaligus.Compiler menerapkannya otomatis berdasarkan isi tipe. Ini alasan Rust mencegah data race: kode yang memindahkan nilai tidak aman ke thread lain ditolak saat kompilasi.
Untuk membagi state yang bisa berubah antar task, bungkus dengan Mutex dalam Arc (shared ownership):
cat > src/main.rs <<'EOF'
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0u32));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = counter.clone();
handles.push(tokio::spawn(async move {
let mut nilai = counter.lock().await;
*nilai += 1;
}));
}
for h in handles {
h.await.unwrap();
}
println!("nilai akhir: {}", *counter.lock().await);
}
EOF
cargo runArc memungkinkan banyak task memegang referensi ke data yang sama, dan Mutex memastikan hanya satu task yang mengubahnya pada satu waktu. Di tokio, pakai tokio::sync::Mutex saat guard melewati titik .await, dan std::sync::Mutex untuk kritikal section yang pendek.
Pola actor: state dimiliki eksklusif oleh satu task, dan task lain berinteraksi lewat channel. Tidak ada mutex yang dibutuhkan karena state hanya disentuh oleh pemiliknya.
cat > src/main.rs <<'EOF'
use tokio::sync::{mpsc, oneshot};
enum Perintah {
Tambah(u32, oneshot::Sender<u32>),
}
async fn actor(mut rx: mpsc::Receiver<Perintah>) {
let mut nilai = 0;
while let Some(perintah) = rx.recv().await {
match perintah {
Perintah::Tambah(n, reply) => {
nilai += n;
let _ = reply.send(nilai);
}
}
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(16);
tokio::spawn(actor(rx));
let (reply, terima) = oneshot::channel();
let _ = tx.send(Perintah::Tambah(5, reply)).await;
println!("nilai: {}", terima.await.unwrap());
}
EOF
cargo runTask actor memegang nilai sebagai state pribadi. Panggilan dikirim lewat mpsc, jawaban dikembalikan lewat oneshot. Karena state tak pernah dibagi, keamanannya terjamin — inilah pola actor-like yang banyak dipakai arsitektur message-passing, dan akarnya terlihat pada framework seperti actix.
Pedoman ringkas: untuk operasi menunggu I/O, pakai async task tokio; untuk pekerjaan CPU-bound, pakai thread (std::thread) atau rayon; untuk state bersama yang kecil, Arc<Mutex>; untuk state yang besar atau akses sering, pertimbangkan pola actor; dan untuk menyalurkan hasil antar task, gunakan channel. Pilihan ini menentukan performa sekaligus kebenaran program kalian.
Inti yang harus dibawa pulang:
tokio::spawn menjalankan task; .await menunggu tanpa memblokir runtime.mpsc, watch, dan broadcast menghubungkan task dengan pola berbeda.Send dan Sync dijamin compiler; data race ditolak saat kompilasi.Di episode 13 selanjutnya kita akan membahas performance tuning dan profiling — memprofil aplikasi Rust dengan perf, tokio-console, dan cargo flamegraph, mengoptimasi memori dan CPU dengan zero-cost abstractions dan iterators, serta menangani bottleneck I/O, allocation, dan lock contention.