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
195: Marker Traits: Send, Sync, Copy
Marker traits are a bit of a trip when you first encounter them because, unlike almost every other trait we've used, they don't have any methods. You don't implement a function to satisfy them. Instead, they are signals to the compiler—essentially labels that say, "This type has this specific property," which allows the compiler to let you do things that would otherwise be dangerous.
To make this concrete, let's build a simple asynchronous command dispatcher. We want to be able to send commands to a background worker thread and have those commands be easy to duplicate for logging purposes.
Making commands trivial to duplicate
I'll start by defining a Command enum. In a real app, this might be "SaveFile" or "RestartServer," but for our example, we'll keep it simple. I want to be able to pass this command to a logger and then still send it to the worker thread without calling .clone() manually every single time.
#[derive(Debug, Clone, Copy)]
enum Command {
Quit,
MoveTo(i32, i32),
Resize(u32, u32),
}
By adding Copy here, I'm telling Rust that Command is a "POD" (Plain Old Data) type. Since it only contains integers, a bitwise copy is perfectly safe. Now, when I assign a Command to a new variable, Rust won't move the value; it'll just copy it. It's a small quality-of-life win, but it's exactly what the Copy marker trait is for.
Moving our commands across thread boundaries
Now, let's get into the multi-threading side of things. I want to spawn a thread that listens for these commands. To do this, I need a way to send data from the main thread to the worker thread. This is where the Send marker trait comes into play.
Most types in Rust are Send by default. If a type is Send, it means it's safe to transfer ownership of that value to another thread. Since our Command enum is just a few integers, the compiler automatically marks it as Send.
use std::thread;
fn main() {
let cmd = Command::MoveTo(10, 20);
thread::spawn(move || {
println!("Worker received: {:?}", cmd);
}).join().unwrap();
}
This works seamlessly. The move keyword transfers ownership of cmd to the new thread, and because Command is Send, the compiler gives us the green light.
The Rc mistake
Here is where I usually trip up when I'm prototyping. Let's say I want the command to carry some shared configuration data—maybe a string that describes the environment. My first instinct might be to use Rc (Reference Counted) because I want multiple parts of my program to hold a handle to this config.
Watch what happens when I try this:
use std::rc::Rc;
use std::thread;
struct Job {
cmd: Command,
config: Rc<String>,
}
fn main() {
let job = Job {
cmd: Command::Quit,
config: Rc::new("Production".to_string()),
};
thread::spawn(move || {
println!("Processing {:?} in {}", job.cmd, job.config);
}).join().unwrap();
}
If you try to compile this, the compiler will scream at you. It'll tell you that Rc<String> cannot be sent between threads safely. Why? Because Rc is not Send. The internal reference counter in Rc is not atomic; if two threads tried to increment it at the same time, you'd have a data race, and the memory would corrupt. The Send marker trait is what prevents this catastrophe from happening at runtime by catching it at compile time.
Safe sharing with Arc and Sync
To fix this, I need to swap Rc for Arc (Atomic Reference Counted). Arc is essentially the thread-safe version of Rc.
But there's a second marker trait involved here: Sync. A type is Sync if it's safe to share a reference to it (&T) between multiple threads. In short: T is Sync if and only if &T is Send.
By switching to Arc, we satisfy both requirements. Arc uses atomic operations for the counter, so it's Send, and because the data inside it is immutable by default, it's also Sync.
use std::sync::Arc;
use std::thread;
struct Job {
cmd: Command,
config: Arc<String>,
}
fn main() {
let job = Job {
cmd: Command::Quit,
config: Arc::new("Production".to_string()),
};
thread::spawn(move || {
println!("Processing {:?} in {}", job.cmd, job.config);
}).join().unwrap();
}
Now it compiles. I didn't have to implement any methods; I just chose a type that had the correct marker traits for my architectural needs. That's the power of marker traits: they turn complex concurrency rules into simple type-checking errors.
📋 Practical Task
Fixing the Multi-Threaded Configuration Shared State
You are working on a system that monitors server health. You have a MonitorConfig struct that needs to be shared across three different worker threads. However, the current implementation is using Rc, which is causing a compilation error because Rc does not implement the Send marker trait.
Your Task:
- Modify the code to use
Arcinstead ofRcso that the configuration can be safely moved into the spawned threads. - Ensure the
Commandenum implements theCopytrait so it can be passed around without explicit cloning.
use std::rc::Rc;
use std::thread;
#[derive(Debug, Clone)]
enum Command {
CheckDisk,
CheckCpu,
}
struct MonitorConfig {
id: u32,
label: String,
}
fn main() {
let config = Rc::new(MonitorConfig {
id: 1,
label: "Main-Server".to_string(),
});
let cmd = Command::CheckCpu;
let mut handles = vec![];
for i in 0..3 {
let thread_config = config.clone();
// The line below currently fails to compile
let handle = thread::spawn(move || {
println!("Thread {} checking {:?} on {}", i, cmd, thread_config.label);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}There are no comments for now.