Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concurrency Testing

🔧 HOW-TO | 📚 REFERENCE | Test thread safety with deterministic thread ordering

Test thread-safe code with deterministic thread ordering using loom.

Why Concurrency Testing?

Normal tests run threads in random order - race conditions may not appear:

#![allow(unused)]
fn main() {
// ❌ This might pass or fail randomly
test!(test_race_condition, {
    let data = Arc::new(Mutex::new(0));
    let data_clone = data.clone();

    thread::spawn(move || {
        *data_clone.lock().unwrap() += 1;
    });

    thread::sleep(Duration::from_millis(1));
    let result = *data.lock().unwrap();
    assert_eq!(result, 1);  // Might fail if thread hasn't run yet
});
}

Loom testing explores all possible interleavings:

#![allow(unused)]
fn main() {
// ✅ This tests all possible thread orderings
test!(test_with_loom, {
    loom::model(|| {
        let data = Arc::new(Mutex::new(0));
        let data_clone = data.clone();

        thread::spawn(move || {
            *data_clone.lock().unwrap() += 1;
        });

        let result = *data.lock().unwrap();
        assert_eq!(result, 1);  // Tests all interleavings
    });
});
}

Basic Loom Testing

Simple Loom Model

#![allow(unused)]
fn main() {
use chicago_tdd_tools::concurrency::*;
use std::sync::{Arc, Mutex};

test!(test_basic_loom, {
    loom::model(|| {
        let data = Arc::new(Mutex::new(0));
        let value = *data.lock().unwrap();
        assert_eq!(value, 0);
    });
});
}

Two Threads

#![allow(unused)]
fn main() {
test!(test_two_threads, {
    loom::model(|| {
        let data = Arc::new(Mutex::new(0));

        let data_clone = data.clone();
        thread::spawn(move || {
            *data_clone.lock().unwrap() += 1;
        });

        let result = *data.lock().unwrap();
        // Loom tests both possible interleavings:
        // 1. Main thread reads first (0)
        // 2. Worker thread increments first (1)
    });
});
}

Real-World Example: Counter

#![allow(unused)]
fn main() {
test!(test_concurrent_counter, {
    loom::model(|| {
        let counter = Arc::new(Mutex::new(0));

        let mut handles = vec![];

        // Spawn 3 threads
        for _ in 0..3 {
            let counter = counter.clone();
            let handle = thread::spawn(move || {
                *counter.lock().unwrap() += 1;
            });
            handles.push(handle);
        }

        // Wait for all threads
        for handle in handles {
            handle.join().unwrap();
        }

        // All threads should have incremented
        assert_eq!(*counter.lock().unwrap(), 3);
    });
});
}

Real-World Example: Channel Communication

#![allow(unused)]
fn main() {
test!(test_channel_communication, {
    loom::model(|| {
        let (tx, rx) = std::sync::mpsc::channel();

        thread::spawn(move || {
            tx.send(42).unwrap();
        });

        let value = rx.recv().unwrap();
        assert_eq!(value, 42);
    });
});
}

Common Concurrency Patterns

Mutex Protection

#![allow(unused)]
fn main() {
test!(test_mutex_safety, {
    loom::model(|| {
        let data = Arc::new(Mutex::new(vec![]));

        let data_clone = data.clone();
        thread::spawn(move || {
            data_clone.lock().unwrap().push(1);
        });

        data.lock().unwrap().push(2);
        let result = data.lock().unwrap();
        assert_eq!(result.len(), 2);
    });
});
}

RwLock (Reader-Writer Lock)

#![allow(unused)]
fn main() {
test!(test_rwlock, {
    loom::model(|| {
        let data = Arc::new(RwLock::new(0));

        let data_clone = data.clone();
        thread::spawn(move || {
            *data_clone.write().unwrap() = 42;
        });

        let value = *data.read().unwrap();
        assert_eq!(value, 42);
    });
});
}

Atomic Operations

#![allow(unused)]
fn main() {
test!(test_atomic, {
    loom::model(|| {
        use std::sync::atomic::{AtomicU32, Ordering};

        let counter = Arc::new(AtomicU32::new(0));

        let counter_clone = counter.clone();
        thread::spawn(move || {
            counter_clone.fetch_add(1, Ordering::SeqCst);
        });

        let value = counter.load(Ordering::SeqCst);
        // Value might be 0 or 1 depending on scheduling
    });
});
}

Detecting Race Conditions

Race Condition Example

#![allow(unused)]
fn main() {
test!(test_detects_race_condition, {
    loom::model(|| {
        let data = Arc::new(Cell::new(0));  // ❌ Not thread-safe!

        let data_clone = data.clone();
        thread::spawn(move || {
            data_clone.set(data_clone.get() + 1);
        });

        // This will fail with loom!
        // Cell doesn't provide synchronization
    });
});
}

Loom detects this because Cell isn't thread-safe.

Use Mutex Instead

#![allow(unused)]
fn main() {
test!(test_thread_safe, {
    loom::model(|| {
        let data = Arc::new(Mutex::new(0));  // ✅ Thread-safe

        let data_clone = data.clone();
        thread::spawn(move || {
            *data_clone.lock().unwrap() += 1;
        });

        let result = *data.lock().unwrap();
        // Now safe for all interleavings
    });
});
}

Testing for Deadlocks

Loom can detect potential deadlocks:

#![allow(unused)]
fn main() {
test!(test_deadlock_detection, {
    loom::model(|| {
        let lock1 = Arc::new(Mutex::new(0));
        let lock2 = Arc::new(Mutex::new(0));

        let (lock1_clone, lock2_clone) = (lock1.clone(), lock2.clone());
        thread::spawn(move || {
            // Thread 1: Lock in order lock1, lock2
            let _g1 = lock1_clone.lock().unwrap();
            let _g2 = lock2_clone.lock().unwrap();
        });

        // Main thread: Lock in opposite order lock2, lock1
        // Loom will explore both interleavings
        // Can detect potential deadlock!
    });
});
}

Best Practices

Do:

  • Test small, focused scenarios
  • Use appropriate synchronization primitives
  • Test with few threads (2-3 typical)
  • Verify all possible interleavings
  • Use Loom for critical concurrent code

Don't:

  • Test large thread pools (explodes combinations)
  • Mix blocking I/O with Loom (I/O not deterministic)
  • Over-test (Loom is slow, only use for critical code)
  • Assume one test covers all cases

Performance

Loom explores all interleavings - it's slow:

  • Simple model (2 threads): 10ms - 100ms
  • Complex model (3 threads): 100ms - 1s
  • Many threads: Can be very slow

Recommendation:

  • Only use Loom for critical synchronization
  • Test with 2-3 threads, not more
  • Use normal tests for non-concurrent code

Limitations

Loom only works with:

  • Loom-aware primitives (loom::sync)
  • Thread creation (loom::thread)
  • Standard Rust types it instruments

Cannot test:

  • Real time (time is controlled)
  • System I/O (returns dummy values)
  • External libraries (unless they use loom)

Combining with Other Techniques

Concurrency + Property-Based

#![allow(unused)]
fn main() {
test!(test_concurrent_property, {
    loom::model(|| {
        let counter = Arc::new(Mutex::new(0));

        for i in 0..5 {
            let counter = counter.clone();
            thread::spawn(move || {
                *counter.lock().unwrap() += i;
            });
        }

        // Verify invariant holds
        let sum: u32 = (0..5).sum();
        assert!(*counter.lock().unwrap() <= sum);
    });
});
}

Real-World Integration Example

#![allow(unused)]
fn main() {
test!(test_thread_pool_safety, {
    loom::model(|| {
        let task_queue = Arc::new(Mutex::new(vec![]));
        let result_queue = Arc::new(Mutex::new(vec![]));

        // Producer
        {
            let queue = task_queue.clone();
            thread::spawn(move || {
                queue.lock().unwrap().push("task1");
            });
        }

        // Consumer
        {
            let task_queue = task_queue.clone();
            let result_queue = result_queue.clone();
            thread::spawn(move || {
                if let Some(task) = task_queue.lock().unwrap().pop() {
                    result_queue.lock().unwrap().push(format!("done: {}", task));
                }
            });
        }

        // Verify result eventually
        // (Loom explores all orderings)
    });
});
}

Troubleshooting

"Too many interleavings"

Reduce complexity:

  • Use fewer threads
  • Smaller critical sections
  • Simpler synchronization patterns

"This synchronization is not supported"

Use only Loom-supported primitives:

  • loom::sync::Mutex
  • loom::sync::RwLock
  • std::sync::atomic
  • std::sync::mpsc

Test Still Hangs/Deadlocks

Loom doesn't catch all deadlocks. Use timeouts:

#![allow(unused)]
fn main() {
#[test]
#[timeout = "5s"]  // Add timeout
fn test_with_timeout() {
    loom::model(|| {
        // Test code
    });
}
}

Next Steps

Learn the "Go the Extra Mile" pattern: Go the Extra Mile


Summary

Concurrency testing with Loom:

  • ✅ Tests all possible thread interleavings
  • ✅ Detects race conditions
  • ✅ Verifies synchronization correctness
  • ✅ Finds potential deadlocks

Use for critical concurrent code to ensure thread safety.