Files
Sprimo/skills/m07-concurrency/comparison.md
2026-02-12 22:58:33 +08:00

7.3 KiB

Concurrency: Comparison with Other Languages

Rust vs Go

Concurrency Model

Aspect Rust Go
Model Ownership + Send/Sync CSP (Communicating Sequential Processes)
Primitives Arc, Mutex, channels goroutines, channels
Safety Compile-time Runtime (race detector)
Async async/await + runtime Built-in scheduler

Goroutines vs Rust Tasks

// Rust: explicit about thread safety
use std::sync::Arc;
use tokio::sync::Mutex;

let data = Arc::new(Mutex::new(vec![]));
let data_clone = Arc::clone(&data);

tokio::spawn(async move {
    let mut guard = data_clone.lock().await;
    guard.push(1);  // Safe: Mutex protects access
});

// Go: implicit sharing (potential race)
// data := []int{}
// go func() {
//     data = append(data, 1)  // RACE CONDITION!
// }()

Channel Comparison

// Rust: typed channels with ownership
use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::channel::<String>(100);

tokio::spawn(async move {
    tx.send("hello".to_string()).await.unwrap();
    // tx is moved, can't be used elsewhere
});

// Go: channels are more flexible but less safe
// ch := make(chan string, 100)
// go func() {
//     ch <- "hello"
//     // ch can still be used anywhere
// }()

Rust vs Java

Thread Safety Model

Aspect Rust Java
Safety Compile-time (Send/Sync) Runtime (synchronized, volatile)
Null No null (Option) NullPointerException risk
Locks RAII (drop releases) try-finally or try-with-resources
Memory No GC GC with stop-the-world

Synchronization Comparison

// Rust: lock is tied to data
use std::sync::Mutex;

let data = Mutex::new(vec![1, 2, 3]);
{
    let mut guard = data.lock().unwrap();
    guard.push(4);
}  // lock released automatically

// Java: lock and data are separate
// List<Integer> data = new ArrayList<>();
// synchronized(data) {
//     data.add(4);
// }  // easy to forget synchronization elsewhere

Thread Pool Comparison

// Rust: rayon for data parallelism
use rayon::prelude::*;

let sum: i32 = (0..1000)
    .into_par_iter()
    .map(|x| x * x)
    .sum();

// Java: Stream API
// int sum = IntStream.range(0, 1000)
//     .parallel()
//     .map(x -> x * x)
//     .sum();

Rust vs C++

Safety Guarantees

Aspect Rust C++
Data races Prevented at compile-time Undefined behavior
Deadlocks Not prevented (same as C++) Not prevented
Thread safety Send/Sync traits Convention only
Memory ordering Explicit Ordering enum memory_order enum

Atomic Comparison

// Rust: clear memory ordering
use std::sync::atomic::{AtomicI32, Ordering};

let counter = AtomicI32::new(0);
counter.fetch_add(1, Ordering::SeqCst);
let value = counter.load(Ordering::Acquire);

// C++: similar but without safety
// std::atomic<int> counter{0};
// counter.fetch_add(1, std::memory_order_seq_cst);
// int value = counter.load(std::memory_order_acquire);

Mutex Comparison

// Rust: data protected by Mutex
use std::sync::Mutex;

struct SafeCounter {
    count: Mutex<i32>,  // Mutex contains the data
}

impl SafeCounter {
    fn increment(&self) {
        *self.count.lock().unwrap() += 1;
    }
}

// C++: mutex separate from data (error-prone)
// class Counter {
//     std::mutex mtx;
//     int count;  // NOT protected by type system
// public:
//     void increment() {
//         std::lock_guard<std::mutex> lock(mtx);
//         count++;
//     }
//     void unsafe_increment() {
//         count++;  // Compiles! But wrong.
//     }
// };

Async Models Comparison

Language Model Runtime
Rust async/await, zero-cost tokio, async-std (bring your own)
Go goroutines Built-in scheduler
JavaScript async/await, Promises Event loop (single-threaded)
Python async/await asyncio (single-threaded)
Java CompletableFuture, Virtual Threads ForkJoinPool, Loom

Rust vs JavaScript Async

// Rust: async requires explicit runtime, can use multiple threads
#[tokio::main]
async fn main() {
    let results = tokio::join!(
        fetch("url1"),  // runs concurrently
        fetch("url2"),
    );
}

// JavaScript: single-threaded event loop
// async function main() {
//     const results = await Promise.all([
//         fetch("url1"),
//         fetch("url2"),
//     ]);
// }

Rust vs Python Async

// Rust: true parallelism possible
#[tokio::main(flavor = "multi_thread")]
async fn main() {
    let handles: Vec<_> = urls
        .into_iter()
        .map(|url| tokio::spawn(fetch(url)))  // spawns on thread pool
        .collect();

    for handle in handles {
        let _ = handle.await;
    }
}

// Python: asyncio is single-threaded (use ProcessPoolExecutor for CPU)
# async def main():
#     tasks = [asyncio.create_task(fetch(url)) for url in urls]
#     await asyncio.gather(*tasks)  # all on same thread

Send and Sync: Rust's Unique Feature

No other mainstream language has compile-time thread safety markers:

Trait Meaning Auto-impl
Send Safe to transfer between threads Most types
Sync Safe to share &T between threads Types with thread-safe &
!Send Must stay on one thread Rc, raw pointers
!Sync References can't be shared RefCell, Cell

Why This Matters

// Rust PREVENTS this at compile time:
use std::rc::Rc;

let rc = Rc::new(42);
std::thread::spawn(move || {
    println!("{}", rc);  // ERROR: Rc is not Send
});

// In other languages, this would be a runtime bug:
// - Go: race detector might catch it
// - Java: undefined behavior
// - Python: GIL usually saves you
// - C++: undefined behavior

Performance Characteristics

Aspect Rust Go Java C++
Thread overhead System threads or M:N M:N (goroutines) System or virtual System threads
Context switch OS-level or cooperative Cheap (goroutines) OS-level OS-level
Memory Predictable (no GC) GC pauses GC pauses Predictable
Async overhead Zero-cost futures Runtime overhead Boxing overhead Depends

When to Use What

Scenario Best Choice
CPU-bound parallelism Rust (rayon), C++
I/O-bound concurrency Rust (tokio), Go, Node.js
Low latency required Rust, C++
Rapid development Go, Python
Complex concurrent state Rust (compile-time safety)

Mental Model Shifts

From Go

Before: "Just use goroutines and channels"
After:  "Explicitly declare what can be shared and how"

Key shifts:

  • Arc<Mutex<T>> instead of implicit sharing
  • Compiler enforces thread safety
  • Async needs explicit runtime

From Java

Before: "synchronized everywhere, hope for the best"
After:  "Types encode thread safety, compiler enforces"

Key shifts:

  • No need for synchronized keyword
  • Mutex contains data, not separate
  • No GC pauses in critical sections

From C++

Before: "Be careful, read the docs, use sanitizers"
After:  "Compiler catches data races, trust the type system"

Key shifts:

  • Send/Sync replace convention
  • RAII locks are mandatory, not optional
  • Much harder to write incorrect concurrent code