ProgrammingParallel Computing Engineer (Rust)

How does safe concurrency work in Rust: what do the marker traits Send and Sync mean, how do they control data transfer and sharing between threads, and how should a developer properly implement (or forbid) these traits for their own types?

Pass interviews with Hintsage AI assistant

Answer.

The issue of safe operation in multithreaded environments has long been faced by programmers, constantly encountering problems such as races, inconsistent data, and memory leaks. Rust has implemented a unique approach with the marker traits Send and Sync to minimize these problems already at the compilation stage.

The problem lies in the lack of access control to shared data between threads, leading to difficult-to-detect errors. In many languages, the responsibility is entirely on the programmer; in Rust, the compiler checks what can be safely passed/shared between threads.

The solution: the Send trait guarantees the safe transfer of an object from one thread to another. Sync allows the shared access to a reference of an object from different threads. Almost all standard types in Rust automatically implement these traits, while custom types can implement them manually or forbid them through impl !Send or impl !Sync for specific cases.

Code example:

use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } // counter will always be equal to 10 without races!

Key features:

  • Send means transferring ownership of an object between threads.
  • Sync means safe shared access to an object through references.
  • Implementing/forbidding traits for custom types allows for controlling behavior at compile time.

Trick Questions.

Can a type with unsafe pointers be Send or Sync?

No, if a type contains a raw pointer or resources without thread safety guarantees, it does not implement these traits, or the developer must implement them manually with full responsibility (usually with unsafe impl Send/Sync).

Are Rc<T> and RefCell<T> Send or Sync?

No, Rc<T> and RefCell<T> are unsafe for concurrent use (neither Send nor Sync). For multithreaded scenarios, Arc<T> and Mutex/RwLock are used.

What happens if a static variable contains a type that does not implement Sync?

Rust will not allow such a static variable to exist: it must be Sync, otherwise the compiler will issue an error.

Common Mistakes and Anti-Patterns

  • Using Rc<T> instead of Arc<T> for shared access from multiple threads.
  • Developing structures with internal unsafe pointers and automatically trusting the Send trait.
  • Violating invariants through the use of unsafe impl Send/Sync without strict control.

Real Life Example

Negative Case

A young developer places an Rc object in thread::spawn - the code only compiles if Rc is not passed between threads. Attempting to take Rc out of thread::spawn results in a compilation error because Rc does not implement Send and is not protected from races.

Pros:

  • The compiler immediately prevents the data race error.

Cons:

  • If one does not know the difference between Rc and Arc, it can be difficult to understand the error.

Positive Case

Arc+Mutex is used for a multithreaded counter, all threads work with the same data through a thread-safe interface. No races, the code is safe and robust.

Pros:

  • No races, memory safety, using marker traits to control behavior.

Cons:

  • Mutex and Arc have overhead, requiring knowledge of thread-safe primitives.