Sync #

Concurrency is one of the most difficult domains in programming — data races, deadlocks, and race conditions are bugs that are hard to reproduce and even harder to debug. In other languages, developers rely on conventions and manual discipline to avoid these problems. Rust takes a fundamentally different approach: the compiler guarantees that no data races can pass to runtime. Not by prohibiting concurrency, but by making unsafe state sharing impossible to compile. This article discusses the concurrency primitives in the Rust standard library — Arc, Mutex, RwLock, channel mpsc, and type Atomic — as well as design patterns that ensure your concurrent code is definitively correct.

Why Rust Concurrency is Different #

In Rust, two traits determine whether a type can be used concurrently:

  • Send — type that can be moved to another thread. Almost all types of Send, except those containing raw pointers or non-thread-safe references such as Rc<T>.
  • Sync — type that can be referenced from another thread (i.e. &T is Send). The type Sync is safe to access from multiple threads simultaneously.

These two traits are implemented automatically by the compiler based on type composition. If you try to send a type that is not Send to another thread, the code will not compile.

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

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

    // CORRECT: Arc is the thread-safe version of 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 -- Yes --> C{Implements Sync?}
    B -- No --> D["Tidak bisa dipindah ke thread lain\nContoh: Rc<T>, *mut T"]

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

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

Threads — Creating and Managing #

Standard library provides std::thread for creating OS threads.

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

fn main() {
    // Create thread — closure is moved to a new thread
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("Thread anak: {}", i);
            thread::sleep(Duration::from_millis(10));
        }
    });

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

    // join() — waits for thread to complete, returns Result
    handle.join().unwrap();

    // Send data to thread with move closure
    let data = vec![1, 2, 3, 4, 5];
    let handle = thread::spawn(move || {
        // data is moved into the thread
        let jumlah: i32 = data.iter().sum();
        jumlah  // return value of the thread
    });

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

    // Thread with name — useful for 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();

    // Runs multiple threads at once
    let handles: Vec<_> = (0..5).map(|i| {
        thread::spawn(move || {
            println!("Worker {} selesai", i);
            i * i  // return value
        })
    }).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 Between Threads #

Arc<T> (Atomically Reference Counted) is a thread-safe version of Rc<T>. It allows multiple threads to have references to the same data, with reference counts managed atomically.

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

fn main() {
    // Arc allows shared ownership between threads
    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, not 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 is still valid here
    println!("Main: {:?}", data);

    // Arc is only for immutable shared data
    // For mutable shared data, combine with 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() is more efficient than regular .clone() — it only increments the reference count atomically, does not copy data. Use Arc::clone(&arc) instead of arc.clone() so that the intent is clear in the code.

Mutex<T> — Mutual Exclusion for Mutable State #

Mutex<T> (Mutual Exclusion) ensures that only one thread can access the data at a time. In Rust, data is guarded inside the Mutex — you can’t access the data without locking the Mutex first.

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

fn main() {
    // Mutex wraps data — cannot be accessed without lock
    let mutex = Mutex::new(0i32);

    // lock() returns MutexGuard — released automatically when exiting the scope
    {
        let mut nilai = mutex.lock().unwrap();  // blocks until lock is available
        *nilai += 1;
    }  // MutexGuard is dropped here — lock removed

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

    // General pattern: Arc<Mutex<T>> for 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);  // release lock before further operation

    hasil.sort();
    println!("{:?}", hasil);  // [0, 1, 2, 3, 4] — order is not guaranteed without sort
}

Avoid Deadlock #

Deadlock occurs when two or more threads are waiting for a lock held by another thread. Rust can’t prevent deadlocks at compile time, but there are patterns that help.

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: different lock sequences between threads — potential deadlock
    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t1 = thread::spawn(move || {
        let _lock_a = a.lock().unwrap();  // take A
        thread::sleep(std::time::Duration::from_millis(1));
        let _lock_b = b.lock().unwrap();  // wait for B — can deadlock if t2 holds B
    });

    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t2 = thread::spawn(move || {
        let _lock_b = b.lock().unwrap();  // take B
        thread::sleep(std::time::Duration::from_millis(1));
        let _lock_a = a.lock().unwrap();  // wait for A — can deadlock if t1 holds A
    });

    // TRUE: always locks in the same order across all threads
    let a = Arc::clone(&mutex_a);
    let b = Arc::clone(&mutex_b);
    let t3 = thread::spawn(move || {
        let _lock_a = a.lock().unwrap();  // is always A first
        let _lock_b = b.lock().unwrap();  // new B
    });

    // CORRECT: release the lock as soon as possible — minimize time in the critical section
    let mutex = Arc::new(Mutex::new(0i32));
    let m = Arc::clone(&mutex);
    thread::spawn(move || {
        // ANTI-PATTERN: long computation in lock
        let mut nilai = m.lock().unwrap();
        let hasil = komputasi_berat();  // blocks other threads during this time
        *nilai = hasil;

        // CORRECT: computing out of lock
        let hasil = komputasi_berat();
        let mut nilai = m.lock().unwrap();
        *nilai = hasil;  // lock is for assignment only
    });

    // try_lock — try lock without blocking
    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) allows multiple readers or one writer simultaneously. It is more efficient than Mutex for scenarios where read operations are much more frequent than write operations.

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

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

    // Multiple readers can run simultaneously
    let handles: Vec<_> = (0..4).map(|i| {
        let data = Arc::clone(&data);
        thread::spawn(move || {
            let baca = data.read().unwrap();  // read lock — can be used simultaneously with other readers
            println!("Reader {}: jumlah = {}", i, baca.iter().sum::<i32>());
            // read lock is released when reading out of the scope
        })
    }).collect();

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

    // Only one writer at a time
    {
        let mut tulis = data.write().unwrap();  // write lock — exclusive
        tulis.push(6);
        println!("Setelah tulis: {:?}", *tulis);
    }  // write lock removed

    // Read again after writing
    println!("Hasil akhir: {:?}", *data.read().unwrap());

    // When is RwLock better than Mutex?
    // RwLock is good for: caches, configurations, registries that are often read but rarely changed
    // Mutex is simpler for: counters, queues, frequently modified states
}
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 — Inter-Thread Communication #

Channel is a message passing-based communication mechanism — threads communicate by sending data, not sharing state. This is often easier to reason about than shared state.

mpsc is an abbreviation for multiple producer, single consumer — many senders, one receiver.

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

fn main() {
    // Create channels — tx to send, rx to receive
    let (tx, rx) = mpsc::channel();

    // Posted from another thread
    let tx_clone = tx.clone();  // clone sender for multiple producers
    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();
    });

    // Receive all messages — recv() blocks until there is a message or all senders are dropped
    for pesan in rx {
        println!("Diterima: {}", pesan);
    }
    // The loop ends when all senders are dropped
}

Producer-Consumer Pattern #

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));  // work simulation
                    diproses += 1;
                }
                Tugas::Selesai => {
                    println!("Worker selesai, total diproses: {}", diproses);
                    break;
                }
            }
        }
    });

    // Producer — submit tasks
    for i in 0..5 {
        tx.send(Tugas::Proses(format!("data-{}", i))).unwrap();
    }
    tx.send(Tugas::Selesai).unwrap();  // stop signal

    worker.join().unwrap();

    // Bounded channel — sync_channel
    let (tx, rx) = mpsc::sync_channel::<i32>(3);  // buffers a maximum of 3 messages

    thread::spawn(move || {
        for i in 0..10 {
            println!("Mengirim {}", i);
            tx.send(i).unwrap();  // blocks if buffer is full
        }
    });

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

try_recv and 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 — does not block, returns immediately
    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 — block with timeout
    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 — Lock-Free Operation #

Type Atomic allows thread-safe read-write operations without locks. They are implemented using special CPU instructions (compare-and-swap, fetch-and-add) that are guaranteed to be atomic.

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

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

    let handles: Vec<_> = (0..10).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            // fetch_add — add and return the old value, atomic
            counter.fetch_add(1, Ordering::SeqCst);
        })
    }).collect();

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

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

    // AtomicBool — thread-safe flag
    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);  // stop signal
    worker.join().unwrap();

    // AtomicUsize — Secure generator ID
    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);  // Each ID is unique, but the order is uncertain
}

Memory Ordering #

Memory ordering determines how atomic operations interact with other memory operations around them.

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

fn main() {
    // Ordering::Relaxed — atomicity only, no ordering guarantee
    // Suitable for: simple counters that do not depend on other values
    let counter = AtomicI32::new(0);
    counter.fetch_add(1, Ordering::Relaxed);

    // Ordering::Release + Acquire — synchronization between producer and consumer
    // Release: all previous writes are visible to the thread that performed the Acquire
    // Suitable for: flag indicating data is ready
    static DATA_SIAP: AtomicBool = AtomicBool::new(false);
    static mut DATA: i32 = 0;

    // Producer thread
    // unsafe { DATA = 42; } // write data
    // DATA_SIAP.store(true, Ordering::Release);   // mark ready

    // Consumer thread
    // while !DATA_SIAP.load(Ordering::Acquire) { } // wait until ready
    // unsafe { println!("{}", DATA); } // safe to read now

    // Ordering::SeqCst — total, strongest and most secure ordering
    // Suitable for: when unsure, or when correctness is more important than performance
    counter.store(0, Ordering::SeqCst);
    counter.fetch_add(1, Ordering::SeqCst);
    println!("{}", counter.load(Ordering::SeqCst));
}
OrderingStrengthPerformanceWhen to Use
RelaxedLowestBestSimple counter without dependencies
AcquireMediumGoodRead the flag indicating data is ready
ReleaseMediumGoodWrite flag once data is ready
AcqRelMediumGoodRead-modify-write(fetch_add, compare_exchange)
SeqCstHighestSlowestSafe default, when unsure
For most cases, use Ordering::SeqCst — it’s easiest to reason about and the performance difference is only significant on hot paths with millions of operations per second. Optimize to weaker ordering only after profiling has proven necessary.

Concurrency Design Patterns #

Simple Thread Pool with Channels #

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();  // sends a stop signal to each 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 with 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 {
        // Check with read lock first (fast)
        if let Some(nilai) = self.get(kunci) {
            return nilai;
        }
        // If none, calculate and save
        let nilai = hitung();
        self.set(kunci.to_string(), nilai.clone());
        nilai
    }
}

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

    // Many readers simultaneously
    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 — Initialize Once #

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(|| {
        // Executes only once, even if called from multiple threads
        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();
    }
    // "Initializing configuration..." appears only once
}

Selecting Appropriate Concurrency Primitives #

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?} -- Yes --> B{Perlu dimodifikasi?}
    A -- No --> C[Arc<T> only — shared immutable]

    B -- Yes --> D{Frekuensi baca vs tulis?}
    B -- No --> C

    D -->|Baca >> Tulis| E[Arc<RwLock<T>><br/>Cache, configuration]
    D -->|Seimbang| F[Arc<Mutex<T>><br/>Counter, queue]
    D -->|Operasi sederhana| G[Atomic<br/>Counter, flag, ID]

    A -- No --> H{Thread perlu berkomunikasi?}
    H -- Yes --> I["mpsc channel\nProducer-consumer"]
    H -- No --> 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

Summary #

  • Send and Sync are compile-time guarantees — the compiler ensures that types that are not thread-safe cannot be sent to another thread. This is the foundation of Rust’s “fearless concurrency.”
  • Arc<T> for shared ownership — thread-safe version of Rc<T>. Use Arc::clone(&arc) instead of arc.clone() so that the intention is clear. Arc itself is only for immutable data.
  • Arc<Mutex<T>> for shared mutable stateMutex guarantees that data can only be accessed by one thread. MutexGuard releases automatically when exiting the scope — take advantage of this to minimize lock time.
  • Arc<RwLock<T>> for read-heavy workloads — many readers can run simultaneously, only writers exclusively. More efficient than Mutex if the read to write ratio is much higher.
  • mpsc channel for message passing — send data between threads with ownership transfer, not sharing. This pattern eliminates the need for locks and is easier to reason with.
  • Atomic for simple lock-free operationsAtomicI32, AtomicBool, AtomicUsize for counters, flags, and ID generators without Mutex overhead. Use SeqCst as the default ordering.
  • Avoid deadlocks with consistent lock order — always lock multiple mutexes in the same order across all threads. Minimize code in critical sections.
  • Choose message passing first — if data can be communicated over channels rather than shared, it’s usually a simpler design and easier to maintain.


← Previous: Collections   Next: Time & Duration →

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