ExamplesBy LevelBy TopicLearning Paths
460 Advanced

460: `Send` and `Sync` Bounds

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "460: `Send` and `Sync` Bounds" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Rust's type system prevents data races at compile time through two auto traits. Key difference from OCaml: 1. **Compile

Tutorial

The Problem

Rust's type system prevents data races at compile time through two auto traits. Send means a type's ownership can be transferred to another thread — its memory can be safely moved across thread boundaries. Sync means a shared reference &T can be safely used from multiple threads simultaneously. These are auto-derived compositionally: a struct is Send if all fields are Send. Rc<T> is !Send because its reference counter isn't atomic. Cell<T> is !Sync because interior mutability without synchronization is unsafe across threads.

Send and Sync are the compile-time foundation of Rust's fearless concurrency — every thread spawn, channel send, and Arc clone depends on them.

🎯 Learning Outcomes

  • • Understand the semantic difference: Send = ownership transfer, Sync = shared reference sharing
  • • Learn which standard types are !Send/!Sync and why: Rc, Cell, RefCell, raw pointers
  • • See how PhantomData<T> marks a type as !Send or !Sync without holding a value
  • • Understand when unsafe impl Send/Sync is needed and the safety invariant it asserts
  • • Learn how Arc<Mutex<T>> achieves Send + Sync by composing individually safe primitives
  • Code Example

    #![allow(clippy::all)]
    //! # Send and Sync Bounds — Thread Safety Markers
    //!
    //! Understanding Send and Sync traits for thread safety.
    //!
    //! - `Send`: Type can be transferred to another thread
    //! - `Sync`: Type can be shared between threads via &T
    
    use std::cell::{Cell, RefCell};
    use std::marker::PhantomData;
    use std::rc::Rc;
    use std::sync::{Arc, Mutex};
    
    /// A type that is both Send and Sync
    /// Most types composed of Send+Sync are automatically Send+Sync
    pub struct ThreadSafe {
        data: i32,
    }
    
    // ThreadSafe is automatically Send + Sync because i32 is
    
    /// A type that is Send but NOT Sync
    /// Example: Mutex<T> - can be moved, but &Mutex can't be accessed without lock
    pub struct SendNotSync {
        /// Cell is not Sync (interior mutability without synchronization)
        data: Cell<i32>,
    }
    
    // Cell<i32> is Send but not Sync
    // Wrapping it doesn't change that
    
    /// A type that is neither Send nor Sync
    /// Example: Rc<T> - reference count is not atomic
    pub struct NotSendNotSync {
        data: Rc<i32>,
    }
    
    // Rc is neither Send nor Sync
    
    /// A type that is Sync but NOT Send (rare)
    /// Example: MutexGuard - borrowed from the Mutex, can't be moved
    pub struct SyncNotSend {
        // Use PhantomData to make it !Send
        _marker: PhantomData<*const ()>,
        data: i32,
    }
    
    // Raw pointers are neither Send nor Sync by default
    
    unsafe impl Sync for SyncNotSend {}
    
    /// Demonstrate Send with Arc
    pub fn demonstrate_send() {
        let data = Arc::new(42);
    
        // Arc<T> is Send if T is Send
        let handle = std::thread::spawn({
            let data = Arc::clone(&data);
            move || *data
        });
    
        assert_eq!(handle.join().unwrap(), 42);
    }
    
    /// Demonstrate Sync with shared reference
    pub fn demonstrate_sync() {
        let data = Arc::new(Mutex::new(0));
    
        // Mutex<T> is Sync if T is Send
        std::thread::scope(|s| {
            for _ in 0..4 {
                let data = &data;
                s.spawn(move || {
                    *data.lock().unwrap() += 1;
                });
            }
        });
    
        assert_eq!(*data.lock().unwrap(), 4);
    }
    
    /// Wrapper that makes a type !Send
    pub struct NoSend<T> {
        inner: T,
        _marker: PhantomData<*const ()>,
    }
    
    impl<T> NoSend<T> {
        pub fn new(inner: T) -> Self {
            Self {
                inner,
                _marker: PhantomData,
            }
        }
    
        pub fn get(&self) -> &T {
            &self.inner
        }
    
        pub fn into_inner(self) -> T {
            self.inner
        }
    }
    
    /// Verify Send/Sync implementations at compile time
    fn assert_send<T: Send>() {}
    fn assert_sync<T: Sync>() {}
    fn assert_send_sync<T: Send + Sync>() {}
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_thread_safe_is_send_sync() {
            assert_send::<ThreadSafe>();
            assert_sync::<ThreadSafe>();
            assert_send_sync::<ThreadSafe>();
        }
    
        #[test]
        fn test_send_not_sync() {
            assert_send::<SendNotSync>();
            // assert_sync::<SendNotSync>(); // Would fail: Cell is not Sync
        }
    
        #[test]
        fn test_common_types() {
            // i32 is Send + Sync
            assert_send_sync::<i32>();
    
            // String is Send + Sync
            assert_send_sync::<String>();
    
            // Vec<T> is Send + Sync if T is
            assert_send_sync::<Vec<i32>>();
    
            // Arc<T> is Send + Sync if T is Send + Sync
            assert_send_sync::<Arc<i32>>();
    
            // Mutex<T> is Send + Sync if T is Send
            assert_send_sync::<Mutex<i32>>();
    
            // RefCell is Send but not Sync
            assert_send::<RefCell<i32>>();
            // assert_sync::<RefCell<i32>>(); // Would fail
        }
    
        #[test]
        fn test_demonstrate_send() {
            demonstrate_send();
        }
    
        #[test]
        fn test_demonstrate_sync() {
            demonstrate_sync();
        }
    
        #[test]
        fn test_no_send_wrapper() {
            let wrapped = NoSend::new(42);
            assert_eq!(*wrapped.get(), 42);
    
            let inner = wrapped.into_inner();
            assert_eq!(inner, 42);
        }
    
        // This test verifies the compile-time checks
        #[test]
        fn test_arc_mutex_pattern() {
            let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    
            let handles: Vec<_> = (0..4)
                .map(|i| {
                    let shared = Arc::clone(&shared);
                    std::thread::spawn(move || {
                        shared.lock().unwrap().push(i);
                    })
                })
                .collect();
    
            for h in handles {
                h.join().unwrap();
            }
    
            let result = shared.lock().unwrap();
            assert_eq!(result.len(), 4);
        }
    }

    Key Differences

  • Compile-time vs. discipline: Rust enforces thread safety at compile time; OCaml 5.x relies on programmer discipline and runtime tools.
  • Auto-derivation: Rust automatically derives Send/Sync based on fields; OCaml has no equivalent automatic annotation.
  • Unsafe override: Rust allows unsafe impl Send for T for manually verified safety; OCaml has no such mechanism.
  • GC interaction: OCaml's GC makes some values inherently thread-unsafe (mutable values shared across domains in OCaml 5.x); Rust's ownership system eliminates this class of bug.
  • OCaml Approach

    OCaml has no equivalent of Send/Sync — the type system doesn't track thread safety. In OCaml 4.x, the GIL ensures only one thread runs OCaml code at a time, making data races impossible. In OCaml 5.x, data races on mutable values are possible and the programmer is responsible for synchronization. There is no compile-time enforcement — OCaml 5.x's safety relies on programming discipline and tools like ThreadSanitizer.

    Full Source

    #![allow(clippy::all)]
    //! # Send and Sync Bounds — Thread Safety Markers
    //!
    //! Understanding Send and Sync traits for thread safety.
    //!
    //! - `Send`: Type can be transferred to another thread
    //! - `Sync`: Type can be shared between threads via &T
    
    use std::cell::{Cell, RefCell};
    use std::marker::PhantomData;
    use std::rc::Rc;
    use std::sync::{Arc, Mutex};
    
    /// A type that is both Send and Sync
    /// Most types composed of Send+Sync are automatically Send+Sync
    pub struct ThreadSafe {
        data: i32,
    }
    
    // ThreadSafe is automatically Send + Sync because i32 is
    
    /// A type that is Send but NOT Sync
    /// Example: Mutex<T> - can be moved, but &Mutex can't be accessed without lock
    pub struct SendNotSync {
        /// Cell is not Sync (interior mutability without synchronization)
        data: Cell<i32>,
    }
    
    // Cell<i32> is Send but not Sync
    // Wrapping it doesn't change that
    
    /// A type that is neither Send nor Sync
    /// Example: Rc<T> - reference count is not atomic
    pub struct NotSendNotSync {
        data: Rc<i32>,
    }
    
    // Rc is neither Send nor Sync
    
    /// A type that is Sync but NOT Send (rare)
    /// Example: MutexGuard - borrowed from the Mutex, can't be moved
    pub struct SyncNotSend {
        // Use PhantomData to make it !Send
        _marker: PhantomData<*const ()>,
        data: i32,
    }
    
    // Raw pointers are neither Send nor Sync by default
    
    unsafe impl Sync for SyncNotSend {}
    
    /// Demonstrate Send with Arc
    pub fn demonstrate_send() {
        let data = Arc::new(42);
    
        // Arc<T> is Send if T is Send
        let handle = std::thread::spawn({
            let data = Arc::clone(&data);
            move || *data
        });
    
        assert_eq!(handle.join().unwrap(), 42);
    }
    
    /// Demonstrate Sync with shared reference
    pub fn demonstrate_sync() {
        let data = Arc::new(Mutex::new(0));
    
        // Mutex<T> is Sync if T is Send
        std::thread::scope(|s| {
            for _ in 0..4 {
                let data = &data;
                s.spawn(move || {
                    *data.lock().unwrap() += 1;
                });
            }
        });
    
        assert_eq!(*data.lock().unwrap(), 4);
    }
    
    /// Wrapper that makes a type !Send
    pub struct NoSend<T> {
        inner: T,
        _marker: PhantomData<*const ()>,
    }
    
    impl<T> NoSend<T> {
        pub fn new(inner: T) -> Self {
            Self {
                inner,
                _marker: PhantomData,
            }
        }
    
        pub fn get(&self) -> &T {
            &self.inner
        }
    
        pub fn into_inner(self) -> T {
            self.inner
        }
    }
    
    /// Verify Send/Sync implementations at compile time
    fn assert_send<T: Send>() {}
    fn assert_sync<T: Sync>() {}
    fn assert_send_sync<T: Send + Sync>() {}
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_thread_safe_is_send_sync() {
            assert_send::<ThreadSafe>();
            assert_sync::<ThreadSafe>();
            assert_send_sync::<ThreadSafe>();
        }
    
        #[test]
        fn test_send_not_sync() {
            assert_send::<SendNotSync>();
            // assert_sync::<SendNotSync>(); // Would fail: Cell is not Sync
        }
    
        #[test]
        fn test_common_types() {
            // i32 is Send + Sync
            assert_send_sync::<i32>();
    
            // String is Send + Sync
            assert_send_sync::<String>();
    
            // Vec<T> is Send + Sync if T is
            assert_send_sync::<Vec<i32>>();
    
            // Arc<T> is Send + Sync if T is Send + Sync
            assert_send_sync::<Arc<i32>>();
    
            // Mutex<T> is Send + Sync if T is Send
            assert_send_sync::<Mutex<i32>>();
    
            // RefCell is Send but not Sync
            assert_send::<RefCell<i32>>();
            // assert_sync::<RefCell<i32>>(); // Would fail
        }
    
        #[test]
        fn test_demonstrate_send() {
            demonstrate_send();
        }
    
        #[test]
        fn test_demonstrate_sync() {
            demonstrate_sync();
        }
    
        #[test]
        fn test_no_send_wrapper() {
            let wrapped = NoSend::new(42);
            assert_eq!(*wrapped.get(), 42);
    
            let inner = wrapped.into_inner();
            assert_eq!(inner, 42);
        }
    
        // This test verifies the compile-time checks
        #[test]
        fn test_arc_mutex_pattern() {
            let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    
            let handles: Vec<_> = (0..4)
                .map(|i| {
                    let shared = Arc::clone(&shared);
                    std::thread::spawn(move || {
                        shared.lock().unwrap().push(i);
                    })
                })
                .collect();
    
            for h in handles {
                h.join().unwrap();
            }
    
            let result = shared.lock().unwrap();
            assert_eq!(result.len(), 4);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_thread_safe_is_send_sync() {
            assert_send::<ThreadSafe>();
            assert_sync::<ThreadSafe>();
            assert_send_sync::<ThreadSafe>();
        }
    
        #[test]
        fn test_send_not_sync() {
            assert_send::<SendNotSync>();
            // assert_sync::<SendNotSync>(); // Would fail: Cell is not Sync
        }
    
        #[test]
        fn test_common_types() {
            // i32 is Send + Sync
            assert_send_sync::<i32>();
    
            // String is Send + Sync
            assert_send_sync::<String>();
    
            // Vec<T> is Send + Sync if T is
            assert_send_sync::<Vec<i32>>();
    
            // Arc<T> is Send + Sync if T is Send + Sync
            assert_send_sync::<Arc<i32>>();
    
            // Mutex<T> is Send + Sync if T is Send
            assert_send_sync::<Mutex<i32>>();
    
            // RefCell is Send but not Sync
            assert_send::<RefCell<i32>>();
            // assert_sync::<RefCell<i32>>(); // Would fail
        }
    
        #[test]
        fn test_demonstrate_send() {
            demonstrate_send();
        }
    
        #[test]
        fn test_demonstrate_sync() {
            demonstrate_sync();
        }
    
        #[test]
        fn test_no_send_wrapper() {
            let wrapped = NoSend::new(42);
            assert_eq!(*wrapped.get(), 42);
    
            let inner = wrapped.into_inner();
            assert_eq!(inner, 42);
        }
    
        // This test verifies the compile-time checks
        #[test]
        fn test_arc_mutex_pattern() {
            let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    
            let handles: Vec<_> = (0..4)
                .map(|i| {
                    let shared = Arc::clone(&shared);
                    std::thread::spawn(move || {
                        shared.lock().unwrap().push(i);
                    })
                })
                .collect();
    
            for h in handles {
                h.join().unwrap();
            }
    
            let result = shared.lock().unwrap();
            assert_eq!(result.len(), 4);
        }
    }

    Deep Comparison

    Send and Sync Traits

    Definitions

  • Send: Can be moved to another thread
  • Sync: Can be shared via &T between threads
  • Common Types

    TypeSendSyncNotes
    i32Primitive
    StringOwned
    Arc<T>✓*✓**if T is
    Mutex<T>✓*✓**if T: Send
    Rc<T>Non-atomic refcount
    Cell<T>✓*Interior mut
    RefCell<T>✓*Interior mut

    Rule

    &T: Send if and only if T: Sync

    Exercises

  • Send wrapper: Implement struct ForceSend<T>(T) with unsafe impl Send for ForceSend<T>. Write a function that wraps an Rc<i32> (which is !Send) in ForceSend and sends it to another thread. In a comment, explain the safety invariant you're asserting and why it's actually unsafe.
  • Sync assertion test: Use static_assertions::assert_impl_all!(T: Send + Sync) (or write your own fn assert_send_sync<T: Send + Sync>()) to verify at compile time that your custom types satisfy the expected bounds. Add tests for Arc<i32>, Mutex<i32>, Arc<Mutex<i32>>, Rc<i32>.
  • Custom concurrent type: Design struct ConcurrentBag<T: Send> wrapping Arc<Mutex<Vec<T>>>. Manually implement Send and Sync with safety proof in comments. Verify it can be shared across threads using Arc::clone.
  • Open Source Repos