Send indicates that a value of a type can be safely transferred across threads, while Sync indicates that shared references can be safely passed across threads. They are compile-time contracts; they do not automatically add locks at runtime, nor do they require internal immutability.
Formally, T: Sync if and only if &T: Send. This definition reduces "sharing an object" to "sending a shared reference to it to another thread." Send, Sync
Conditions for Common Types
The table below discusses common default-allocator types, omitting some generic and lifetime details:
Type
Send Condition
Sync Condition
Reason
Vec, Box
T: Send
T: Sync
Ownership and borrowing propagate to inner elements
Cell, RefCell
T: Send
Not Sync
Can be moved exclusively, but cannot be shared for arbitrary concurrent access
Rc
Not Send
Not Sync
Non-atomic reference counting may be accessed concurrently by clones
Arc
T: Send + Sync
T: Send + Sync
Reference counting is atomic; does not automatically synchronize T
Mutex
T: Send
T: Send
Serializes internal access via locks
RwLock
T: Send
T: Send + Sync
Shared read locks allow multiple threads to read T simultaneously
*const T, *mut T
Not implemented by default
Not implemented by default
Raw pointers cannot automatically prove resource and thread contracts
RefCell<u32> is Send but not Sync, which shows that the two are not the same "thread-safety switch." Arc<RefCell<u32>> also cannot be shared across threads just because Arc is added: Arc only maintains the ownership count, and the borrow state of RefCell does not become synchronized.
Mutex<T>'s Sync only requires T: Send because the lock allows different threads to access T exclusively in sequence, rather than sharing read access to T simultaneously. std::sync::MutexGuard does not implement Send for platform portability, to avoid releasing a lock on another thread that some platforms require to be released by the same thread; this does not conflict with Mutex's own conditions.
Moving RefCell vs. Sharing a Reference to RefCell
usestd::cell::RefCell;usestd::thread;fnmain(){let local =RefCell::new(vec![1,2]);let returned =thread::spawn(move||{
local.borrow_mut().push(3);
local
}).join().unwrap();assert_eq!(&*returned.borrow(),&[1,2,3]);}
The main thread hands over ownership of the entire RefCell to the worker thread, which returns it upon completion; no two threads operate on its dynamic borrow state simultaneously, so RefCell<Vec<i32>> can satisfy Send. If instead you pass &local to another thread while retaining access in the original thread, RefCell would need to satisfy Sync, which it does not.
Rc has stricter restrictions: even if there happens to be only one strong reference at runtime, Rc<T>'s type does not temporarily implement Send because of it. Compile-time constraints cannot derive long-term ownership proof from a single count query. You can try to extract T when it is unique, and then pass T; or if you truly need to share across threads, choose Arc which satisfies the conditions.
Why Mutex Can Wrap Non-Sync T
usestd::cell::RefCell;usestd::sync::Mutex;usestd::thread;fnmain(){let value =Mutex::new(RefCell::new(0u32));thread::scope(|scope|{for_in0..2{let shared =&value;
scope.spawn(move||{let guard = shared.lock().unwrap();*guard.borrow_mut()+=1;});}});assert_eq!(value.into_inner().unwrap().into_inner(),2);}
Mutex ensures that threads accessing RefCell enter the critical section sequentially, so RefCell itself does not need to be Sync here. This combination is used to demonstrate trait derivation; in practice, this counter only needs Mutex<u32>; once you have exclusive access via MutexGuard, adding another layer of RefCell is often unnecessary.
RwLock's read guard, however, allows multiple threads to obtain shared references to the interior simultaneously, so its Sync condition also requires T: Sync. You cannot use "it has a lock" as a reason to assume all lock types satisfy the same conditions.
Derive Send and Sync layer by layer
Use RefCell inside Arc and then Mutex, comparing Sync. What matters is how the wrapper accesses T: Arc allows shared access, while Mutex serializes exclusive access across threads. Switching T to Rc exposes the separate limitation that T itself is not Send.
T:Sync⟺&T:Send
Preparing the visual
Derive Send and Sync layer by layer
Arc does not make a non-Sync value shareable; Mutex is Sync when T is Send.
usestd::cell::RefCell;usestd::sync::{Arc, Mutex};fnassert_send<T: Send>(){}fnassert_sync<T: Sync>(){}fnmain(){assert_send::<RefCell<u32>>();assert_send::<Arc<String>>();assert_sync::<Arc<String>>();assert_sync::<Mutex<RefCell<u32>>>();// assert_sync::<RefCell<u32>>(); // Does not satisfy Sync
// assert_send::<Arc<RefCell<u32>>>(); // Arc does not supplement internal synchronization
}
The structural derivation of auto traits is the default rule, but it is also influenced by explicit implementations on the type itself. It is not true that "except for raw pointers, Rc, and a few Cells, all other types are definitely Send/Sync"; platform handles, thread-local objects, and third-party abstractions may also have restrictions.
Send and 'static Are Different Axes
Ordinary thread::spawn usually requires the closure to be Send + 'static. Send governs cross-thread usage, while 'static governs captured borrows, ensuring they do not outlive the independent thread; 'static does not mean the value must live forever.
Moving a String into a closure satisfies the condition of having no short-lived borrows, but moving a &str only moves the reference, without extending the lifetime of the referenced object. For threads that are guaranteed to complete before the scope ends, you can use thread::scope to borrow outer data; see Threads and Synchronization.
Whether an async Future is Send also depends on the state it may hold. Non-Send values surviving across suspension points often prevent the Future from being spawned on a cross-thread scheduler; do not casually use unsafe impl Send just to eliminate errors. You can narrow the scope of values or lock guards, or use local task execution mechanisms as needed.
Scoped Threads Relax Lifetimes, Not Send/Sync
usestd::thread;fnmain(){letmut values =[1,2,3,4];thread::scope(|scope|{let(left, right)= values.split_at_mut(2);
scope.spawn(move||{for value in left {*value *=10;}});
scope.spawn(move||{for value in right {*value +=1;}});});assert_eq!(values,[10,20,4,5]);}
split_at_mut establishes non-overlapping regions, and threads obtain exclusive references to them; scope ensures these threads finish before leaving the scope, allowing borrowing of the local array. This avoids copying the entire array and does not require Arc to extend its ownership. Scoped Threads
However, scope does not make RcSend, nor does it make &RefCell<T> safely shareable across threads. It solves whether borrows can remain valid, not the synchronization of shared access.
What is transferred
Core condition required
T
T: Send
&T
T: Sync
&mut T
T: Send; the original thread cannot simultaneously use this exclusive borrow
Closure with borrows
Captured actual fields satisfy thread requirements; borrows cover the thread execution period
Future State Depends on Suspension Points and Initial Captures
usestd::rc::Rc;fnassert_send<T: Send>(_: T){}
async fnlocal_work(){{let local =Rc::new(String::from("temporary"));assert_eq!(local.len(),9);}std::future::ready(()).await;}fnmain(){assert_send(local_work());}
Here, Rc is created after the async function starts being polled, and its usage ends and it is released before the await, so the Future can satisfy Send. The example only checks the Future's type, without running the executor; assertions in the function body only execute when polled.
If Rc is captured via async move from outside, the Future owns it before the first poll. Even if the function body prepares to drop it before the first await, you cannot simply apply "no cross-await means Send." When diagnosing, check initial captures, local values saved during suspension, and return value requirements simultaneously. Specific runtime spawn implementations may also require Future::Output: Send, which is a separate constraint from the Future itself being Send.
PhantomData Expresses Logical Relationships, Not Synchronization
Even if handle is just an integer, the marker expresses that this wrapper should not automatically be Send/Sync. PhantomData does not store a real Rc, but it participates in the derivation of auto traits, variance, and drop checks; changing the PhantomData type may simultaneously change multiple meanings, not just toggle a single thread attribute button.
Conversely, adding PhantomData<T> does not automatically prove Send for a container containing raw pointers. When you really need to write unsafe impl, you must prove that all public operations, callbacks, and drops involving moving/sharing adhere to thread contracts, rather than claiming "the underlying library is probably thread-safe."
unsafe impl Must Cover Drops and Aliasing, Not Just One Read/Write Operation
When wrapping external handles, even if all methods are locked, if the underlying requirement is to destroy the handle on the thread that created it, allowing the owner to move to any thread may still violate the contract. Similarly, if a method returns an internal pointer that bypasses synchronization, or if a callback can access the same state outside the lock, it breaks the seemingly protected model.
You should check item by item: who owns the resource, what aliases can exist, which thread performs the drop, and whether all public operations maintain the same rules. If the underlying API is inherently thread-bound, you should send tasks back to the thread owning the handle, or retain an explicit non-Send wrapper. Hand-written unsafe impl must have proofs consistent with these paths, not just citing "used Mutex."
Compiling Does Not Equal Concurrent Business Correctness
Arc<Mutex<Account>> can avoid unsynchronized data access, but it does not automatically turn operations across two accounts into atomic transactions, nor does it eliminate lock-order deadlocks. Sync's requirement for interior mutability is for safe synchronization, not to guarantee that every group of multi-step operations satisfies business invariants.
When designing interfaces, first decide on sharing, transferring, or message passing, and then let trait conditions reflect the actual model. Send/Sync errors usually indicate that some resource or lifetime decision has not yet been clarified.