Sync

Sync #

Konkurensi adalah salah satu domain yang paling sulit dalam pemrograman — data race, deadlock, dan kondisi balapan adalah bug yang sulit direproduksi dan lebih sulit lagi di-debug. Di bahasa lain, developer bergantung pada konvensi dan disiplin manual untuk menghindari masalah ini. Rust mengambil pendekatan yang fundamental berbeda: kompiler menjamin tidak ada data race bisa lolos ke runtime. Bukan dengan melarang konkurensi, tapi dengan menjadikan state sharing yang tidak aman mustahil untuk dikompilasi. Artikel ini membahas primitif konkurensi di standard library Rust — Arc, Mutex, RwLock, channel mpsc, dan tipe Atomic — serta pola desain yang memastikan kode konkuren kamu benar secara definitif.

Mengapa Konkurensi Rust Berbeda #

Di Rust, dua trait menentukan apakah suatu tipe bisa digunakan secara konkuren:

  • Send — tipe yang bisa dipindahkan ke thread lain. Hampir semua tipe Send, kecuali yang mengandung pointer mentah atau reference non-thread-safe seperti Rc<T>.
  • Sync — tipe yang bisa direferensikan dari thread lain (yaitu &T bersifat Send). Tipe yang Sync aman diakses dari banyak thread secara bersamaan.

Kedua trait ini di-implementasikan secara otomatis oleh compiler berdasarkan komposisi tipe. Jika kamu mencoba mengirim tipe yang tidak Send ke thread lain, kode tidak akan dikompilasi.

use std::thread;
use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // ANTI-PATTERN: Rc tidak Send — tidak bisa dikirim ke thread lain
    let rc = Rc::new(42);
    // thread::spawn(move || println!("{}", rc));
    // ERROR: `Rc<i32>` cannot be sent between threads safely

    // BENAR: Arc adalah versi thread-safe dari Rc
    let arc = Arc::new(42);
    let arc_clone = Arc::clone(&arc);
    thread::spawn(move || {
        println!("Di thread lain: {}", arc_clone);
    }).join().unwrap();

    println!("Di main thread: {}", arc);
}
flowchart TD
    A[Tipe T] --> B{Implements Send?}
    B -- Ya --> C{Implements Sync?}
    B -- Tidak --> D["Tidak bisa dipindah ke thread lain\nContoh: Rc<T>, *mut T"]

    C -- Ya --> E["Aman untuk shared reference antar thread\nContoh: Arc<T>, Mutex<T>"]
    C -- Tidak --> F["Bisa dipindah tapi tidak di-share\nContoh: Cell<T>, RefCell<T>"]

    style D fill:#ffebee
    style E fill:#e8f5e9
    style F fill:#fff3e0

Thread — Membuat dan Mengelola #

Standard library menyediakan std::thread untuk membuat thread OS.

use std::thread;
use std::time::Duration;

fn main() {
    // Membuat thread — closure di-move ke thread baru
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("Thread anak: {}", i);
            thread::sleep(Duration::from_millis(10));
        }
    });

    // Main thread terus berjalan
    for i in 0..3 {
        println!("Main thread: {}", i);
        thread::sleep(Duration::from_millis(15));
    }

    // join() — tunggu thread selesai, mengembalikan Result
    handle.join().unwrap();

    // Mengirim data ke thread dengan move closure
    let data = vec![1, 2, 3, 4, 5];
    let handle = thread::spawn(move || {
        // data di-move ke dalam thread
        let jumlah: i32 = data.iter().sum();
        jumlah  // nilai return dari thread
    });

    let hasil = handle.join().unwrap();  // ambil nilai return
    println!("Jumlah: {}", hasil);

    // Thread dengan nama — berguna untuk debugging
    let handle = thread::Builder::new()
        .name("worker-thread".to_string())
        .stack_size(4 * 1024 * 1024)  // 4MB stack
        .spawn(|| {
            println!("Thread: {:?}", thread::current().name());
        })
        .unwrap();
    handle.join().unwrap();

    // Menjalankan banyak thread sekaligus
    let handles: Vec<_> = (0..5).map(|i| {
        thread::spawn(move || {
            println!("Worker {} selesai", i);
            i * i  // return nilai
        })
    }).collect();

    let hasil: Vec<i32> = handles.into_iter()
        .map(|h| h.join().unwrap())
        .collect();
    println!("{:?}", hasil);  // [0, 1, 4, 9, 16]
}

Arc<T> — Shared Ownership Antar Thread #

Arc<T> (Atomically Reference Counted) adalah versi thread-safe dari Rc<T>. Ia memungkinkan banyak thread memiliki referensi ke data yang sama, dengan reference count yang dikelola secara atomic.

use std::sync::Arc;
use std::thread;

fn main() {
    // Arc memungkinkan shared ownership antar thread
    let data = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);  // clone Arc, bukan data
        let handle = thread::spawn(move || {
            println!("Thread {}: jumlah = {}", i, data_clone.iter().sum::<i32>());
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    // data masih valid di sini
    println!("Main: {:?}", data);

    // Arc hanya untuk immutable shared data
    // Untuk mutable shared data, kombinasikan dengan Mutex
    let counter = Arc::new(std::sync::Mutex::new(0i32));

    let handles: Vec<_> = (0..5).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            let mut nilai = counter.lock().unwrap();
            *nilai += 1;
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Counter akhir: {}", *counter.lock().unwrap());  // 5
}
Arc::clone() lebih efisien dari .clone() biasa — ia hanya menambah reference count secara atomic, tidak menyalin data. Gunakan Arc::clone(&arc) bukan arc.clone() agar niat jelas di kode.

Mutex<T> — Mutual Exclusion untuk Mutable State #

Mutex<T> (Mutual Exclusion) memastikan hanya satu thread yang bisa mengakses data di dalamnya pada satu waktu. Di Rust, data dijaga di dalam Mutex — kamu tidak bisa mengakses data tanpa mengunci Mutex terlebih dahulu.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Mutex membungkus data — tidak bisa diakses tanpa lock
    let mutex = Mutex::new(0i32);

    // lock() mengembalikan MutexGuard — released otomatis saat keluar scope
    {
        let mut nilai = mutex.lock().unwrap();  // blok sampai lock tersedia
        *nilai += 1;
    }  // MutexGuard di-drop di sini — lock dilepas

    println!("{}", mutex.lock().unwrap());  // 1

    // Pola umum: Arc<Mutex<T>> untuk shared mutable state
    let shared = Arc::new(Mutex::new(Vec::<i32>::new()));

    let handles: Vec<_> = (0..5).map(|i| {
        let shared = Arc::clone(&shared);
        thread::spawn(move || {
            let mut data = shared.lock().unwrap();
            data.push(i);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    let data = shared.lock().unwrap();
    let mut hasil = data.clone();
    drop(data);  // lepas lock sebelum operasi lebih lanjut

    hasil.sort();
    println!("{:?}", hasil);  // [0, 1, 2, 3, 4] — urutan tidak dijamin tanpa sort
}

Menghindari Deadlock #

Deadlock terjadi saat dua atau lebih thread saling menunggu lock yang dipegang oleh thread lain. Rust tidak bisa mencegah deadlock di compile time, tapi ada pola yang membantu.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let mutex_a = Arc::new(Mutex::new(0i32));
    let mutex_b = Arc::new(Mutex::new(0i32));

    // ANTI-PATTERN: urutan lock yang berbeda antar thread — potensi deadlock
    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t1 = thread::spawn(move || {
        let _lock_a = a.lock().unwrap();  // ambil A
        thread::sleep(std::time::Duration::from_millis(1));
        let _lock_b = b.lock().unwrap();  // tunggu B — bisa deadlock jika t2 pegang B
    });

    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t2 = thread::spawn(move || {
        let _lock_b = b.lock().unwrap();  // ambil B
        thread::sleep(std::time::Duration::from_millis(1));
        let _lock_a = a.lock().unwrap();  // tunggu A — bisa deadlock jika t1 pegang A
    });

    // BENAR: selalu kunci dalam urutan yang sama di semua thread
    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t3 = thread::spawn(move || {
        let _lock_a = a.lock().unwrap();  // selalu A dulu
        let _lock_b = b.lock().unwrap();  // baru B
    });

    // BENAR: lepas lock sesegera mungkin — minimalkan waktu di dalam critical section
    let mutex = Arc::new(Mutex::new(0i32));
    let m = Arc::clone(&mutex);
    thread::spawn(move || {
        // ANTI-PATTERN: komputasi panjang di dalam lock
        let mut nilai = m.lock().unwrap();
        let hasil = komputasi_berat();  // blok thread lain selama ini
        *nilai = hasil;

        // BENAR: komputasi di luar lock
        let hasil = komputasi_berat();
        let mut nilai = m.lock().unwrap();
        *nilai = hasil;  // lock hanya untuk assignment
    });

    // try_lock — coba lock tanpa memblok
    let mutex = Mutex::new(42);
    match mutex.try_lock() {
        Ok(nilai) => println!("Berhasil lock: {}", *nilai),
        Err(_) => println!("Lock sedang dipakai, coba lagi nanti"),
    }

    t1.join().unwrap();
    t2.join().unwrap();
    t3.join().unwrap();
}

fn komputasi_berat() -> i32 { 42 }

RwLock<T> — Multiple Readers, One Writer #

RwLock<T> (Read-Write Lock) memungkinkan banyak reader atau satu writer secara bersamaan. Ini lebih efisien dari Mutex untuk skenario di mana operasi baca jauh lebih sering dari operasi tulis.

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3, 4, 5]));

    // Banyak reader bisa berjalan bersamaan
    let handles: Vec<_> = (0..4).map(|i| {
        let data = Arc::clone(&data);
        thread::spawn(move || {
            let baca = data.read().unwrap();  // read lock — bisa bersamaan dengan reader lain
            println!("Reader {}: jumlah = {}", i, baca.iter().sum::<i32>());
            // read lock dilepas saat baca keluar scope
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    // Hanya satu writer pada satu waktu
    {
        let mut tulis = data.write().unwrap();  // write lock — eksklusif
        tulis.push(6);
        println!("Setelah tulis: {:?}", *tulis);
    }  // write lock dilepas

    // Baca lagi setelah tulis
    println!("Hasil akhir: {:?}", *data.read().unwrap());

    // Kapan RwLock lebih baik dari Mutex?
    // RwLock bagus untuk: cache, konfigurasi, registry yang sering dibaca tapi jarang diubah
    // Mutex lebih sederhana untuk: counter, queue, state yang sering dimodifikasi
}
sequenceDiagram
    participant T1 as Thread 1 (Reader)
    participant T2 as Thread 2 (Reader)
    participant T3 as Thread 3 (Writer)
    participant RW as RwLock

    T1->>RW: read()
    RW-->>T1: ReadGuard ✓
    T2->>RW: read()
    RW-->>T2: ReadGuard ✓ (bersamaan dengan T1)
    T3->>RW: write()
    Note over T3,RW: Menunggu T1 dan T2 selesai
    T1->>RW: drop ReadGuard
    T2->>RW: drop ReadGuard
    RW-->>T3: WriteGuard ✓ (sekarang eksklusif)
    T3->>RW: drop WriteGuard

Channel mpsc — Komunikasi Antar Thread #

Channel adalah mekanisme komunikasi berbasis message passing — thread berkomunikasi dengan mengirim data, bukan berbagi state. Ini sering lebih mudah di-reason daripada shared state.

mpsc adalah singkatan dari multiple producer, single consumer — banyak sender, satu receiver.

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

fn main() {
    // Membuat channel — tx untuk kirim, rx untuk terima
    let (tx, rx) = mpsc::channel();

    // Kirim dari thread lain
    let tx_clone = tx.clone();  // clone sender untuk multiple producer
    thread::spawn(move || {
        tx_clone.send(String::from("pesan dari thread 1")).unwrap();
        tx_clone.send(String::from("pesan kedua dari thread 1")).unwrap();
    });

    thread::spawn(move || {
        tx.send(String::from("pesan dari thread 2")).unwrap();
    });

    // Terima semua pesan — recv() memblok sampai ada pesan atau semua sender ter-drop
    for pesan in rx {
        println!("Diterima: {}", pesan);
    }
    // Loop berakhir saat semua sender ter-drop
}

Pola Producer-Consumer #

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

#[derive(Debug)]
enum Tugas {
    Proses(String),
    Selesai,
}

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

    // Worker thread — consumer
    let worker = thread::spawn(move || {
        let mut diproses = 0;
        loop {
            match rx.recv().unwrap() {
                Tugas::Proses(data) => {
                    println!("Memproses: {}", data);
                    thread::sleep(Duration::from_millis(10));  // simulasi kerja
                    diproses += 1;
                }
                Tugas::Selesai => {
                    println!("Worker selesai, total diproses: {}", diproses);
                    break;
                }
            }
        }
    });

    // Producer — kirim tugas
    for i in 0..5 {
        tx.send(Tugas::Proses(format!("data-{}", i))).unwrap();
    }
    tx.send(Tugas::Selesai).unwrap();  // sinyal berhenti

    worker.join().unwrap();

    // Channel dengan batas (bounded channel) — sync_channel
    let (tx, rx) = mpsc::sync_channel::<i32>(3);  // buffer maksimal 3 pesan

    thread::spawn(move || {
        for i in 0..10 {
            println!("Mengirim {}", i);
            tx.send(i).unwrap();  // memblok jika buffer penuh
        }
    });

    for pesan in rx {
        thread::sleep(Duration::from_millis(50));  // consumer lebih lambat
        println!("Diterima: {}", pesan);
    }
}

try_recv dan recv_timeout #

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

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

    thread::spawn(move || {
        thread::sleep(Duration::from_millis(100));
        tx.send(42).unwrap();
    });

    // try_recv — tidak memblok, segera kembali
    match rx.try_recv() {
        Ok(nilai) => println!("Diterima: {}", nilai),
        Err(mpsc::TryRecvError::Empty) => println!("Belum ada pesan"),
        Err(mpsc::TryRecvError::Disconnected) => println!("Sender sudah ter-drop"),
    }

    // recv_timeout — memblok dengan batas waktu
    match rx.recv_timeout(Duration::from_millis(200)) {
        Ok(nilai) => println!("Diterima dalam timeout: {}", nilai),
        Err(mpsc::RecvTimeoutError::Timeout) => println!("Timeout!"),
        Err(mpsc::RecvTimeoutError::Disconnected) => println!("Disconnected"),
    }
}

Atomic — Operasi Lock-Free #

Tipe Atomic memungkinkan operasi baca-tulis yang aman secara thread tanpa lock. Mereka diimplementasikan menggunakan instruksi CPU khusus (compare-and-swap, fetch-and-add) yang dijamin atomic.

use std::sync::atomic::{AtomicI32, AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    // AtomicI32 — counter tanpa Mutex
    let counter = Arc::new(AtomicI32::new(0));

    let handles: Vec<_> = (0..10).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            // fetch_add — tambah dan kembalikan nilai lama, secara atomic
            counter.fetch_add(1, Ordering::SeqCst);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Counter: {}", counter.load(Ordering::SeqCst));  // 10

    // AtomicBool — flag thread-safe
    let berjalan = Arc::new(AtomicBool::new(true));

    let berjalan_clone = Arc::clone(&berjalan);
    let worker = thread::spawn(move || {
        let mut iterasi = 0;
        while berjalan_clone.load(Ordering::Relaxed) {
            iterasi += 1;
            thread::sleep(std::time::Duration::from_millis(1));
        }
        println!("Worker berhenti setelah {} iterasi", iterasi);
    });

    thread::sleep(std::time::Duration::from_millis(10));
    berjalan.store(false, Ordering::Relaxed);  // sinyal berhenti
    worker.join().unwrap();

    // AtomicUsize — ID generator yang aman
    static COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn generate_id() -> usize {
        COUNTER.fetch_add(1, Ordering::SeqCst)
    }

    let handles: Vec<_> = (0..5).map(|_| {
        thread::spawn(|| generate_id())
    }).collect();

    let ids: Vec<usize> = handles.into_iter().map(|h| h.join().unwrap()).collect();
    println!("{:?}", ids);  // setiap ID unik, tapi urutan tidak tentu
}

Memory Ordering #

Memory ordering menentukan bagaimana operasi atomic berinteraksi dengan operasi memori lainnya di sekitarnya.

use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};

fn main() {
    // Ordering::Relaxed — hanya atomicity, tanpa jaminan urutan
    // Cocok untuk: counter sederhana yang tidak bergantung pada nilai lain
    let counter = AtomicI32::new(0);
    counter.fetch_add(1, Ordering::Relaxed);

    // Ordering::Release + Acquire — sinkronisasi antara producer dan consumer
    // Release: semua tulis sebelumnya visible ke thread yang melakukan Acquire
    // Cocok untuk: flag yang menandakan data sudah siap
    static DATA_SIAP: AtomicBool = AtomicBool::new(false);
    static mut DATA: i32 = 0;

    // Producer thread
    // unsafe { DATA = 42; }                       // tulis data
    // DATA_SIAP.store(true, Ordering::Release);   // tandai siap

    // Consumer thread
    // while !DATA_SIAP.load(Ordering::Acquire) {} // tunggu sampai siap
    // unsafe { println!("{}", DATA); }            // aman baca sekarang

    // Ordering::SeqCst — urutan total, paling kuat dan paling aman
    // Cocok untuk: ketika tidak yakin, atau ketika correctness lebih penting dari performa
    counter.store(0, Ordering::SeqCst);
    counter.fetch_add(1, Ordering::SeqCst);
    println!("{}", counter.load(Ordering::SeqCst));
}
OrderingKekuatanPerformaKapan Digunakan
RelaxedTerendahTerbaikCounter sederhana tanpa dependensi
AcquireSedangBaikBaca flag yang menandakan data siap
ReleaseSedangBaikTulis flag setelah data siap
AcqRelSedangBaikRead-modify-write (fetch_add, compare_exchange)
SeqCstTertinggiPaling lambatDefault aman, ketika tidak yakin
Untuk sebagian besar kasus, gunakan Ordering::SeqCst — ini paling mudah di-reason dan perbedaan performanya hanya signifikan di hot path dengan jutaan operasi per detik. Optimasi ke ordering yang lebih lemah hanya setelah profiling membuktikan perlunya.

Pola Desain Konkurensi #

Thread Pool Sederhana dengan Channel #

use std::sync::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;

type Job = Box<dyn FnOnce() + Send + 'static>;

struct ThreadPool {
    workers: Vec<thread::JoinHandle<()>>,
    sender: mpsc::Sender<Option<Job>>,
}

impl ThreadPool {
    fn new(ukuran: usize) -> Self {
        let (sender, receiver) = mpsc::channel::<Option<Job>>();
        let receiver = Arc::new(Mutex::new(receiver));

        let workers = (0..ukuran).map(|id| {
            let receiver = Arc::clone(&receiver);
            thread::spawn(move || loop {
                let pesan = receiver.lock().unwrap().recv().unwrap();
                match pesan {
                    Some(job) => {
                        println!("Worker {} mengerjakan tugas", id);
                        job();
                    }
                    None => {
                        println!("Worker {} berhenti", id);
                        break;
                    }
                }
            })
        }).collect();

        ThreadPool { workers, sender }
    }

    fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
        self.sender.send(Some(Box::new(f))).unwrap();
    }

    fn shutdown(self) {
        for _ in &self.workers {
            self.sender.send(None).unwrap();  // kirim sinyal berhenti ke setiap worker
        }
        for worker in self.workers {
            worker.join().unwrap();
        }
    }
}

fn main() {
    let pool = ThreadPool::new(4);

    for i in 0..8 {
        pool.execute(move || {
            println!("Tugas {} selesai di thread {:?}", i, thread::current().id());
        });
    }

    pool.shutdown();
}

Shared Cache dengan RwLock #

use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;

struct Cache {
    data: RwLock<HashMap<String, String>>,
}

impl Cache {
    fn new() -> Arc<Self> {
        Arc::new(Cache {
            data: RwLock::new(HashMap::new()),
        })
    }

    fn get(&self, kunci: &str) -> Option<String> {
        self.data.read().unwrap().get(kunci).cloned()
    }

    fn set(&self, kunci: String, nilai: String) {
        self.data.write().unwrap().insert(kunci, nilai);
    }

    fn get_or_compute(&self, kunci: &str, hitung: impl FnOnce() -> String) -> String {
        // Cek dengan read lock dulu (cepat)
        if let Some(nilai) = self.get(kunci) {
            return nilai;
        }
        // Jika tidak ada, hitung dan simpan
        let nilai = hitung();
        self.set(kunci.to_string(), nilai.clone());
        nilai
    }
}

fn main() {
    let cache = Cache::new();

    // Banyak reader bersamaan
    let handles: Vec<_> = (0..5).map(|i| {
        let cache = Arc::clone(&cache);
        thread::spawn(move || {
            let kunci = format!("kunci-{}", i % 3);
            let nilai = cache.get_or_compute(&kunci, || {
                println!("Menghitung untuk {}", kunci);
                format!("nilai-{}", i)
            });
            println!("Thread {}: {} = {}", i, kunci, nilai);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

Once — Inisialisasi Sekali #

use std::sync::Once;
use std::thread;

static INIT: Once = Once::new();
static mut KONFIGURASI: Option<String> = None;

fn dapatkan_konfigurasi() -> &'static str {
    INIT.call_once(|| {
        // Hanya dieksekusi sekali, meskipun dipanggil dari banyak thread
        println!("Menginisialisasi konfigurasi...");
        unsafe {
            KONFIGURASI = Some(String::from("konfigurasi-global"));
        }
    });
    unsafe { KONFIGURASI.as_ref().unwrap() }
}

fn main() {
    let handles: Vec<_> = (0..5).map(|_| {
        thread::spawn(|| {
            println!("Konfigurasi: {}", dapatkan_konfigurasi());
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
    // "Menginisialisasi konfigurasi..." hanya muncul sekali
}

Memilih Primitif Konkurensi yang Tepat #

Gunakan Arc<Mutex<T>> jika:
  ✓ Data perlu dimodifikasi dari banyak thread
  ✓ Operasi baca dan tulis sama seringnya
  ✓ Seksi kritis singkat (kurang dari beberapa mikrodetik)

Gunakan Arc<RwLock<T>> jika:
  ✓ Data jauh lebih sering dibaca dari dimodifikasi
  ✓ Operasi baca bisa berjalan lama (tidak apa-apa reader lain ikut)
  ✓ Contoh: cache, konfigurasi, registry

Gunakan mpsc channel jika:
  ✓ Thread berkomunikasi dengan mengirim pesan
  ✓ Ownership data perlu berpindah antar thread (bukan shared)
  ✓ Pola producer-consumer yang jelas
  ✓ Ingin menghindari shared state sepenuhnya

Gunakan Atomic jika:
  ✓ Operasinya sederhana: increment, flag, ID generator
  ✓ Perlu performa maksimum tanpa overhead lock
  ✓ Tidak ada dependensi antar field yang berbeda

Hindari shared mutable state jika:
  ✗ Komunikasi antar thread bisa digantikan message passing
  ✗ Data bisa di-partition — setiap thread punya datanya sendiri
  ✗ Hasil bisa dikumpulkan setelah thread selesai (join + merge)
flowchart TD
    A{Data perlu dibagi antar thread?} -- Ya --> B{Perlu dimodifikasi?}
    A -- Tidak --> C["Arc<T> saja — shared immutable"]

    B -- Ya --> D{Frekuensi baca vs tulis?}
    B -- Tidak --> C

    D -- Baca >> Tulis --> E["Arc<RwLock<T>>\nCache, konfigurasi"]
    D -- Seimbang --> F["Arc<Mutex<T>>\nCounter, queue"]
    D -- Operasi sederhana --> G["Atomic\nCounter, flag, ID"]

    A -- Tidak --> H{Thread perlu berkomunikasi?}
    H -- Ya --> I["mpsc channel\nProducer-consumer"]
    H -- Tidak --> J["Thread independen\nJoin + collect hasil"]

    style C fill:#e8f5e9
    style E fill:#e3f2fd
    style F fill:#fff3e0
    style G fill:#e8f5e9
    style I fill:#fce4ec

Ringkasan #

  • Send dan Sync adalah jaminan compile-time — compiler memastikan tipe yang tidak thread-safe tidak bisa dikirim ke thread lain. Ini adalah fondasi “fearless concurrency” Rust.
  • Arc<T> untuk shared ownership — versi thread-safe dari Rc<T>. Gunakan Arc::clone(&arc) bukan arc.clone() agar niat jelas. Arc sendiri hanya untuk data immutable.
  • Arc<Mutex<T>> untuk shared mutable stateMutex menjamin data hanya bisa diakses oleh satu thread. MutexGuard dilepas otomatis saat keluar scope — manfaatkan ini untuk meminimalkan waktu lock.
  • Arc<RwLock<T>> untuk read-heavy workload — banyak reader bisa berjalan bersamaan, hanya writer yang eksklusif. Lebih efisien dari Mutex jika rasio baca jauh lebih tinggi dari tulis.
  • mpsc channel untuk message passing — kirim data antar thread dengan transfer ownership, bukan sharing. Pola ini menghilangkan kebutuhan lock dan lebih mudah di-reason.
  • Atomic untuk operasi lock-free sederhanaAtomicI32, AtomicBool, AtomicUsize untuk counter, flag, dan ID generator tanpa overhead Mutex. Gunakan SeqCst sebagai default ordering.
  • Hindari deadlock dengan urutan lock konsisten — selalu kunci multiple mutex dalam urutan yang sama di semua thread. Minimalkan kode di dalam critical section.
  • Pilih message passing lebih dulu — jika data bisa dikomunikasikan lewat channel daripada di-share, itu biasanya desain yang lebih sederhana dan lebih mudah di-maintain.

← Sebelumnya: Collections   Berikutnya: Time & Duration →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact