398: Auto Traits (`Send`, `Sync`, `Unpin`)
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
Send means ownership can transfer between threads; Sync means shared references can be accessed from multiple threadsRc<T> is !Send/!Sync (non-atomic reference counting) while Arc<T> is Send + Syncunsafe impl Send for T is needed and what safety invariant it assertsUnpin and its role in pinning for async codeCode 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
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>();
}
}#[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
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.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.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.