Skip to content

rust-memory-safety-examples examples/data_race_prevention.rs: Code Companion

Reference code for the Concurrent Code Introduction lecture. Sections correspond to the lecture document.


Section 1: From Memory Safety to Thread Safety

//! Data race prevention example

// This single use statement brings in all the demonstration functions
// The module system ensures clean separation of concerns
use rust_memory_safety_examples::data_race;

The use statement imports from a library crate, meaning these demonstrations live in src/lib.rs or a submodule. This separation allows the examples to be tested independently from the binary.


Section 2: The Main Function Structure

fn main() {
    println!("=== Data Race Prevention in Rust ===\n");

    // Each demonstration is clearly numbered and labeled
    println!("1. Safe Concurrent Access with Arc and Mutex");
    data_race::safe_concurrent_access();

    println!("\n2. Type System Prevents Races");
    data_race::type_system_prevents_races();

    println!("\n3. C vs Rust Comparison");
    data_race::compare_c_vs_rust();

    // Summary section reinforces key concepts
    println!("\n=== Key Takeaways ===");
    // ... takeaways follow
}

The function calls execute sequentially, ensuring each concurrent demonstration completes before the next begins. This prevents any cross-contamination between examples.


Section 3: Safe Concurrent Access with Arc and Mutex

// Conceptual structure (from the library module):
// Arc<Mutex<T>> combines two guarantees:

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

// Arc: Atomic Reference Counting - safe to clone across threads
// Mutex: Mutual Exclusion - only one thread accesses inner data at a time
let shared_data: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new()));

// Clone Arc to share ownership with spawned thread
let data_for_thread = Arc::clone(&shared_data);

thread::spawn(move || {
    // .lock() returns a Result<MutexGuard<T>, PoisonError>
    // The guard provides exclusive access until dropped
    let mut guard = data_for_thread.lock().unwrap();
    guard.push(42);  // Safe: we hold the lock
});  // Guard drops here, releasing the lock

The MutexGuard returned by .lock() implements Deref and DerefMut, so you can use it like &mut T. The lock is automatically released when the guard goes out of scope.


Section 4: The Type System as Thread Safety Enforcer

// Send and Sync are auto-traits - the compiler derives them automatically

// This compiles: i32 is Send
thread::spawn(move || {
    let x: i32 = 42;
    println!("{}", x);
});

// This would NOT compile: Rc is not Send
// use std::rc::Rc;
// let rc = Rc::new(42);
// thread::spawn(move || {
//     println!("{}", rc);  // ERROR: Rc<i32> cannot be sent between threads safely
// });

// The thread::spawn signature enforces Send:
// pub fn spawn<F, T>(f: F) -> JoinHandle<T>
// where
//     F: FnOnce() -> T + Send + 'static,  // <-- F must be Send
//     T: Send + 'static,                   // <-- Return type must be Send

The compiler error for non-Send types is precise: "Rc cannot be sent between threads safely." This specificity helps developers understand exactly what needs to change.


Section 5: Connecting to Ownership Fundamentals

// The move keyword transfers ownership into the closure
let data = vec![1, 2, 3];

let handle = thread::spawn(move || {
    // data is now owned by this thread's closure
    // The original thread can no longer access it
    println!("Thread has: {:?}", data);
});

// This would NOT compile:
// println!("{:?}", data);  // ERROR: value borrowed after move

handle.join().unwrap();

// Without move, the compiler checks lifetimes:
// let local = String::from("hello");
// thread::spawn(|| {
//     println!("{}", local);  // ERROR: local may be dropped while thread runs
// });

The 'static bound on thread::spawn requires that all captured data either be owned (move) or have 'static lifetime. This prevents dangling references across thread boundaries.


Section 6: C vs Rust Comparison

// In C (pseudocode showing the problem):
// int shared_counter = 0;
// pthread_mutex_t lock;  // The lock exists, but...
//
// void* thread_func(void* arg) {
//     shared_counter++;  // Oops! Forgot to acquire lock
//     return NULL;       // Data race: undefined behavior
// }

// In Rust, the type system prevents this:
let counter = Arc::new(Mutex::new(0));

let counter_clone = Arc::clone(&counter);
thread::spawn(move || {
    // Cannot access the i32 without going through the Mutex
    // counter_clone += 1;  // ERROR: no += on Arc<Mutex<i32>>

    // Must acquire lock to access the inner value
    let mut num = counter_clone.lock().unwrap();
    *num += 1;  // Safe: lock is held
});

The Mutex<T> wrapper makes the protected data inaccessible without acquiring the lock. There's no way to "forget" to lock—the type system physically prevents unguarded access.


Section 7: The Key Takeaways Section

println!("\n=== Key Takeaways ===");
println!("✓ Rust prevents data races at compile time");
println!("✓ Send and Sync traits enforce thread safety");
println!("✓ Arc and Mutex provide safe concurrent access");
println!("✓ No undefined behavior in concurrent code");

Each takeaway maps to a concrete mechanism: compile-time prevention via the borrow checker, Send/Sync traits for type-level enforcement, Arc<Mutex<T>> for shared mutable state, and the absence of undefined behavior in safe Rust.


Quick Reference

Type Purpose Thread Safety
Rc<T> Single-threaded reference counting Not Send, not Sync
Arc<T> Multi-threaded reference counting Send + Sync if T: Send + Sync
Mutex<T> Mutual exclusion wrapper Sync if T: Send
RwLock<T> Reader-writer lock Sync if T: Send + Sync
Trait Meaning Auto-derived?
Send Safe to transfer to another thread Yes
Sync Safe to share references across threads Yes
// Common pattern: shared mutable state across threads
let shared: Arc<Mutex<T>> = Arc::new(Mutex::new(value));

// Clone for each thread
let thread_copy = Arc::clone(&shared);

// Spawn and move ownership of the clone
thread::spawn(move || {
    let mut guard = thread_copy.lock().unwrap();
    // Use guard as &mut T
});