Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
43: Send and Sync Traits Explained
Most of the time, you won't actually write impl Send for MyStruct {}. In fact, you rarely will. These are "marker traits," meaning they don't have any methods to implement; they just tell the compiler, "Hey, this type is safe to behave this way." But when the compiler starts screaming at you because you're trying to pass a variable into std::thread::spawn, you need to understand what's happening under the hood.
Think of a high-end professional cinema camera.
- Send is like the ability to put that camera in a shipping crate and mail it to a producer in another city. Once it's shipped, you no longer have it; the producer now owns it and controls it.
- Sync is like putting that camera on a tripod in a studio and letting five different assistants look through the viewfinder at the same time. No one is moving the camera to a new city; they're all just referencing the same physical object in one place.
Now, let's map that directly to Rust. Send means ownership of a value can be transferred between threads. If a type is Send, you can move it into a new thread. Sync means a type can be safely referenced by multiple threads simultaneously. Specifically, T is Sync if and only if a reference to it (&T) is Send. It sounds circular, but it's the core of Rust's concurrency safety.
Moving the Burden across Threads
Most types in Rust are Send. An i32, a String, or a custom struct containing other Send types are all fine to move. I usually only run into Send issues when I'm dealing with raw pointers or some very specific FFI (Foreign Function Interface) bindings.
The most common "aha!" moment comes when you encounter Rc<T>. You know Rc is for reference counting in a single thread. If you try to move an Rc into a thread, the compiler will stop you. Why? Because Rc is not Send. If two threads both held an Rc to the same data and tried to clone it at the same time, they would both try to increment the reference count. Since that counter isn't atomic, you'd have a data race, and the count would get corrupted. The compiler prevents this disaster by marking Rc as !Send (not Send).
Sharing the View safely
Then there's Sync. This is about shared access. If you have a Mutex<T>, you can share it across threads. The Mutex ensures that even though multiple threads have a reference to the lock (meaning the Mutex is Sync), only one thread can actually touch the data inside at a time.
Here is the tricky part: a type can be Send but not Sync. Consider RefCell<T>. You can move a RefCell into another thread (it's Send), but you cannot share a reference to a RefCell across threads (it's not Sync). This is because RefCell performs its borrow checking at runtime using a non-atomic counter. If two threads tried to borrow the same RefCell simultaneously, they'd clash over that internal counter, leading to undefined behavior.
// This is a mental shorthand for how the compiler sees these:
// T: Sync <==> &T: Send
// If I can safely send a reference to you,
// then the original object must be safe to share.
Where the Compiler Saves Your Skin
I've spent plenty of hours staring at compiler errors that look like: the trait bound `Rc<Config>: Send` is not satisfied. When you see this, don't fight the compiler. It's not being pedantic; it's telling you that your data structure is fundamentally incompatible with multi-threading.
Usually, the fix is a direct swap. If you need to move a reference-counted pointer across threads, you swap Rc for Arc (Atomic Reference Counted). Arc uses atomic operations for the counter, which makes it both Send and Sync. It's slightly heavier on performance, but it's the price you pay for not having your program crash randomly in production.
📋 Practical Task
Converting a Reference-Counted Config for Multi-threaded Access
You are working on a system where a Config object is shared across the application. Currently, it uses Rc because the app was single-threaded. However, you've just been asked to implement a background telemetry thread that needs access to this config.
The following code will not compile. Your task is to modify it so that the Config can be safely shared with the spawned thread. You must ensure that the code compiles and runs without changing the logic of how the config is accessed.
use std::rc::Rc;
use std::thread;
struct Config {
api_key: String,
timeout: u32,
}
fn main() {
let config = Rc::new(Config {
api_key: "SECRET_123".to_string(),
timeout: 30,
});
let config_clone = Rc::clone(&config);
let handle = thread::spawn(move || {
println!("Telemetry thread starting with API key: {}", config_clone.api_key);
});
handle.join().unwrap();
println!("Main thread finished.");
}
There are no comments for now.