๐Ÿฆ€ Functional Rust
๐ŸŽฌ Fearless Concurrency Threads, Arc>, channels โ€” safe parallelism enforced by the compiler.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข std::thread::spawn creates OS threads โ€” closures must be Send + 'static

โ€ข Arc> provides shared mutable state across threads safely

โ€ข Channels (mpsc) enable message passing โ€” multiple producers, single consumer

โ€ข Send and Sync marker traits enforce thread safety at compile time

โ€ข Data races are impossible โ€” the type system prevents them before your code runs

125: Send and Sync

Difficulty: 3 Level: Intermediate Compile-time thread safety: `Send` lets you move a value to another thread; `Sync` lets you share a reference between threads.

The Problem This Solves

Data races are one of the hardest bugs to debug: two threads access the same memory simultaneously, at least one writes, and there's no synchronization. In most languages, including OCaml, the programmer must manually ensure thread safety โ€” the compiler won't stop you from sharing a non-thread-safe type across threads. Rust makes data races a compile error. The mechanism is two marker traits: `Send` and `Sync`. `Send` means "it's safe to transfer ownership of this value to another thread." `Sync` means "it's safe to share a reference to this value between threads" โ€” which is equivalent to `&T: Send`. Most types are both `Send` and `Sync` automatically โ€” the compiler derives these for any type whose fields are all `Send`/`Sync`. The exceptions are explicit: `Rc<T>` is neither (non-atomic reference count), `Cell<T>` is `Send` but not `Sync` (interior mutability without synchronization), raw pointers are neither. When you write `thread::spawn(move || { ... })`, the closure must be `Send`. If you accidentally capture an `Rc` or a `Cell`, the compiler refuses with a clear error. No segfault at 3am โ€” a clear message at `cargo build`.

The Intuition

`Send` = can be transferred to another thread. `Sync` = can be shared between threads. The compiler checks these automatically โ€” if a type contains anything non-thread-safe, it inherits that restriction.

How It Works in Rust

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

// Vec<i32> is Send โ€” ownership can transfer to another thread
let data = vec![1, 2, 3, 4, 5];
let handle = thread::spawn(move || data.iter().sum::<i32>());
assert_eq!(handle.join().unwrap(), 15);

// Arc<Mutex<T>> is both Send and Sync โ€” the standard pattern for
// shared mutable state across threads
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
 let counter = Arc::clone(&counter);  // Arc is Sync: safe to clone across threads
 handles.push(thread::spawn(move || {
     *counter.lock().unwrap() += 1;   // Mutex ensures only one thread at a time
 }));
}
for h in handles { h.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 10);

// Types that are NOT Send โ€” compiler catches misuse:
use std::rc::Rc;
let rc = Rc::new(42);
// thread::spawn(move || *rc);  // ERROR: `Rc<i32>` cannot be sent between threads safely
// Rc uses non-atomic reference counting โ€” race condition waiting to happen.
// Use Arc<T> instead for thread-safe shared ownership.

// You can verify at compile time:
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<String>();       // โœ“
assert_send::<Arc<i32>>();     // โœ“
assert_sync::<Arc<i32>>();     // โœ“
assert_sync::<Mutex<i32>>();   // โœ“
// assert_send::<Rc<i32>>();   // โœ— โ€” compile error

What This Unlocks

Key Differences

ConceptOCamlRust
Thread safety enforcementRuntime โ€” programmer's responsibilityCompile time โ€” `Send`/`Sync` trait bounds
Data racesPossible โ€” no compiler checkImpossible โ€” compile error
Shared ownership across threadsMutex + GC`Arc<T>` (requires `T: Send + Sync`)
Shared mutable stateMutex (manual discipline)`Arc<Mutex<T>>` (enforced by types)
Cost of the safety checkNone at compile time, runtime riskZero runtime cost โ€” types only
// Example 125: Send and Sync Marker Traits
//
// Send: type can be transferred to another thread
// Sync: type can be shared (via &T) between threads
// These are auto-implemented and enforced at compile time.

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

// Approach 1: Send โ€” transferring ownership to threads
fn approach1() {
    // Vec<i32> is Send โ€” can be moved to another thread
    let data = vec![1, 2, 3, 4, 5];
    let handle = thread::spawn(move || {
        data.iter().sum::<i32>()
    });
    let sum = handle.join().unwrap();
    assert_eq!(sum, 15);
    println!("Sum: {} (sent Vec to thread)", sum);
    
    // String is Send
    let msg = String::from("hello from main");
    let handle = thread::spawn(move || {
        println!("Thread received: {}", msg);
        msg.len()
    });
    assert_eq!(handle.join().unwrap(), 15);
}

// Approach 2: Sync โ€” sharing references between threads
fn approach2() {
    // Arc<Mutex<T>> is both Send and Sync
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }));
    }
    
    for h in handles {
        h.join().unwrap();
    }
    
    assert_eq!(*counter.lock().unwrap(), 10);
    println!("Counter: {} (Mutex + Arc = thread-safe)", counter.lock().unwrap());
}

// Approach 3: Channel-based message passing (Send required)
fn approach3() {
    let (tx, rx) = mpsc::channel();
    
    // Spawn producers
    for i in 1..=5 {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send(i).unwrap(); // i32 is Send
        });
    }
    drop(tx); // close the sender
    
    let sum: i32 = rx.iter().sum();
    assert_eq!(sum, 15);
    println!("Channel sum: {}", sum);
}

// Types that are NOT Send/Sync
fn _not_send_sync_examples() {
    use std::rc::Rc;
    use std::cell::Cell;
    
    let _rc = Rc::new(42);
    // thread::spawn(move || *_rc); // ERROR: Rc is not Send
    
    let _cell = Cell::new(42);
    // Can't share &Cell between threads: Cell is not Sync
}

fn main() {
    println!("=== Approach 1: Send (Transfer) ===");
    approach1();
    println!("\n=== Approach 2: Sync (Share) ===");
    approach2();
    println!("\n=== Approach 3: Channels ===");
    approach3();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_send_to_thread() {
        let v = vec![1, 2, 3];
        let h = thread::spawn(move || v.len());
        assert_eq!(h.join().unwrap(), 3);
    }

    #[test]
    fn test_arc_is_sync() {
        let a = Arc::new(42);
        let a2 = Arc::clone(&a);
        let h = thread::spawn(move || *a2);
        assert_eq!(h.join().unwrap(), 42);
        assert_eq!(*a, 42);
    }

    #[test]
    fn test_mutex_shared_mutation() {
        let m = Arc::new(Mutex::new(Vec::new()));
        let m2 = Arc::clone(&m);
        let h = thread::spawn(move || {
            m2.lock().unwrap().push(1);
        });
        h.join().unwrap();
        m.lock().unwrap().push(2);
        assert_eq!(m.lock().unwrap().len(), 2);
    }

    #[test]
    fn test_channel_communication() {
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || tx.send(42).unwrap());
        assert_eq!(rx.recv().unwrap(), 42);
    }

    fn _assert_send<T: Send>() {}
    fn _assert_sync<T: Sync>() {}

    #[test]
    fn test_marker_traits() {
        _assert_send::<String>();
        _assert_send::<Vec<i32>>();
        _assert_send::<Arc<i32>>();
        _assert_sync::<Arc<i32>>();
        _assert_sync::<Mutex<i32>>();
        // Rc is NOT Send: _assert_send::<Rc<i32>>(); // won't compile
    }
}
(* Example 125: Send and Sync โ€” Thread Safety Guarantees *)

(* OCaml 5 has domains for parallelism. Before that, the GIL
   prevented true parallelism. Send/Sync concepts don't exist. *)

(* Approach 1: Thread-safe immutable data *)
let approach1 () =
  let data = [1; 2; 3; 4; 5] in
  (* In OCaml 5 with domains, immutable data is inherently safe *)
  let sum = List.fold_left ( + ) 0 data in
  assert (sum = 15);
  Printf.printf "Sum: %d (thread-safe: immutable)\n" sum

(* Approach 2: Mutex for shared mutable state *)
let approach2 () =
  let counter = Mutex.create () in
  let count = ref 0 in
  (* Simulating thread-safe mutation *)
  for _ = 1 to 10 do
    Mutex.lock counter;
    incr count;
    Mutex.unlock counter
  done;
  assert (!count = 10);
  Printf.printf "Counter: %d (mutex-protected)\n" !count

(* Approach 3: Message passing via channels *)
let approach3 () =
  (* OCaml doesn't have std channels like Rust, but we can simulate *)
  let queue = Queue.create () in
  for i = 1 to 5 do
    Queue.add i queue
  done;
  let sum = ref 0 in
  while not (Queue.is_empty queue) do
    sum := !sum + Queue.pop queue
  done;
  assert (!sum = 15);
  Printf.printf "Channel sum: %d\n" !sum

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Send and Sync

Sending Data to Threads

OCaml:

๐Ÿช Show OCaml equivalent
(* OCaml 5 domains โ€” no Send concept *)
let d = Domain.spawn (fun () ->
List.fold_left ( + ) 0 data  (* just use it *)
)

Rust:

let data = vec![1, 2, 3]; // Vec is Send
thread::spawn(move || {
 data.iter().sum::<i32>() // ownership transferred
});
// Rc is NOT Send:
// let rc = Rc::new(42);
// thread::spawn(move || *rc); // COMPILE ERROR

Shared Mutable State

OCaml:

๐Ÿช Show OCaml equivalent
let counter = Mutex.create () in
let count = ref 0 in
(* Programmer must remember to lock/unlock *)
Mutex.lock counter; incr count; Mutex.unlock counter

Rust:

let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || {
 *c.lock().unwrap() += 1;
 // Mutex MUST be locked to access data โ€” enforced by types
});

Compile-Time vs Runtime Safety

OCaml โ€” data race possible:

๐Ÿช Show OCaml equivalent
(* Nothing prevents sharing a ref between domains unsafely *)

Rust โ€” impossible:

// The type system prevents:
// - Sending Rc to threads (not Send)
// - Sharing &Cell between threads (not Sync)
// - Accessing Mutex data without locking