ExamplesBy LevelBy TopicLearning Paths
398 Advanced

398: Auto Traits (`Send`, `Sync`, `Unpin`)

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "398: Auto Traits (`Send`, `Sync`, `Unpin`)" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Thread safety cannot be determined by looking at a type in isolation — it depends on all the types it contains. Key difference from OCaml: 1. **Compile

Tutorial

The Problem

Thread safety cannot be determined by looking at a type in isolation — it depends on all the types it contains. Rust's auto traits (Send, Sync, Unpin) are automatically implemented by the compiler for any type whose fields all satisfy the same trait. If a struct contains only Send fields, it is automatically Send. If it contains a Rc<T> (which is !Send), it is automatically !Send. This compositional property means you never have to manually declare thread safety for most types — the compiler tracks it transitively.

Send and Sync are the foundation of Rust's fearless concurrency: Arc<T> requires T: Send + Sync, thread::spawn requires the closure to be Send, and channel endpoints require Send values.

🎯 Learning Outcomes

  • • Understand how auto traits propagate automatically through composite types
  • • Learn the semantic difference: Send means ownership can transfer between threads; Sync means shared references can be accessed from multiple threads
  • • See why Rc<T> is !Send/!Sync (non-atomic reference counting) while Arc<T> is Send + Sync
  • • Understand when unsafe impl Send for T is needed and what safety invariant it asserts
  • • Learn about Unpin and its role in pinning for async code
  • Code Example

    #![allow(clippy::all)]
    //! Auto Traits: Send, Sync, Unpin
    
    use std::sync::Arc;
    
    pub fn is_send<T: Send>() {}
    pub fn is_sync<T: Sync>() {}
    pub fn is_unpin<T: Unpin>() {}
    
    pub fn check_auto_traits() {
        is_send::<i32>();
        is_sync::<i32>();
        is_send::<String>();
        is_sync::<String>();
        is_send::<Arc<i32>>();
        is_sync::<Arc<i32>>();
        // is_send::<Rc<i32>>(); // Would fail - Rc is !Send
        // is_sync::<RefCell<i32>>(); // Would fail - RefCell is !Sync
    }
    
    pub struct MySendSync {
        pub data: Arc<String>,
    }
    unsafe impl Send for MySendSync {}
    unsafe impl Sync for MySendSync {}
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_i32_send() {
            fn assert_send<T: Send>() {}
            assert_send::<i32>();
        }
        #[test]
        fn test_i32_sync() {
            fn assert_sync<T: Sync>() {}
            assert_sync::<i32>();
        }
        #[test]
        fn test_arc_send_sync() {
            fn assert_both<T: Send + Sync>() {}
            assert_both::<Arc<i32>>();
        }
        #[test]
        fn test_string_send() {
            fn assert_send<T: Send>() {}
            assert_send::<String>();
        }
    }

    Key Differences

  • Compile-time vs. runtime: Rust enforces thread safety at compile time via auto traits; OCaml enforces it at runtime via locks (OCaml 4.x) or domain isolation (OCaml 5.x).
  • Opt-in vs. opt-out: Rust types are Send/Sync by default (if fields are); opting out requires !Send/!Sync via PhantomData. OCaml has no type-level thread safety.
  • **unsafe escape hatch**: Rust allows unsafe impl Send/Sync for manually verified correctness; OCaml trusts the programmer entirely without a special annotation.
  • **Unpin**: Rust's Unpin enables safe movement of pinned values (key for async); OCaml has no pinning concept since values don't have stable addresses.
  • OCaml Approach

    OCaml's runtime is single-threaded by default (the Global Interpreter Lock in OCaml 4.x), so thread safety is not enforced by the type system. OCaml 5.x with effects introduces parallel domains, but uses a different concurrency model without Send/Sync equivalents. Thread safety in OCaml is achieved through Mutex.t and Atomic.t modules, but there is no compile-time enforcement.

    Full Source

    #![allow(clippy::all)]
    //! Auto Traits: Send, Sync, Unpin
    
    use std::sync::Arc;
    
    pub fn is_send<T: Send>() {}
    pub fn is_sync<T: Sync>() {}
    pub fn is_unpin<T: Unpin>() {}
    
    pub fn check_auto_traits() {
        is_send::<i32>();
        is_sync::<i32>();
        is_send::<String>();
        is_sync::<String>();
        is_send::<Arc<i32>>();
        is_sync::<Arc<i32>>();
        // is_send::<Rc<i32>>(); // Would fail - Rc is !Send
        // is_sync::<RefCell<i32>>(); // Would fail - RefCell is !Sync
    }
    
    pub struct MySendSync {
        pub data: Arc<String>,
    }
    unsafe impl Send for MySendSync {}
    unsafe impl Sync for MySendSync {}
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_i32_send() {
            fn assert_send<T: Send>() {}
            assert_send::<i32>();
        }
        #[test]
        fn test_i32_sync() {
            fn assert_sync<T: Sync>() {}
            assert_sync::<i32>();
        }
        #[test]
        fn test_arc_send_sync() {
            fn assert_both<T: Send + Sync>() {}
            assert_both::<Arc<i32>>();
        }
        #[test]
        fn test_string_send() {
            fn assert_send<T: Send>() {}
            assert_send::<String>();
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_i32_send() {
            fn assert_send<T: Send>() {}
            assert_send::<i32>();
        }
        #[test]
        fn test_i32_sync() {
            fn assert_sync<T: Sync>() {}
            assert_sync::<i32>();
        }
        #[test]
        fn test_arc_send_sync() {
            fn assert_both<T: Send + Sync>() {}
            assert_both::<Arc<i32>>();
        }
        #[test]
        fn test_string_send() {
            fn assert_send<T: Send>() {}
            assert_send::<String>();
        }
    }

    Deep Comparison

    OCaml vs Rust: 398-auto-traits

    Exercises

  • Thread safety test: Write a test that spawns 4 threads, each calling a function fn use_it<T: Send + Sync>(val: Arc<T>). Pass it Arc<i32>, Arc<String>, and Arc<Vec<u8>>. Then try passing Arc<RefCell<i32>> and observe the compile error.
  • Custom Send type: Create a SafeCounter struct wrapping AtomicU64. Manually implement unsafe impl Send for SafeCounter and unsafe impl Sync for SafeCounter with a code comment explaining the safety invariant you're asserting.
  • !Send propagation: Create a NotSendStruct { rc: Rc<i32> } and write a test that verifies (using a compile-fail test or static_assertions crate) that it is not Send. Explain why the Rc field causes this.
  • Open Source Repos