Skip to main content

Smart Pointers, Shared State, and Concurrency Basics

Watch First

Why This Matters

Rust makes shared state explicit. That is a strength, but it can feel verbose if learners reach for Arc<Mutex<_>> as the default answer.

The engineering skill is deciding when to own, share, lock, copy, pass messages, or redesign the state boundary.

What You Will Build

Build a small concurrent event counter and worker prototype using channels, Arc, and a lock only where the lock is justified.

Concept

Smart pointers describe ownership patterns:

  • Box<T>: one owner, heap allocation.
  • Rc<T>: shared ownership on one thread.
  • Arc<T>: shared ownership across threads or tasks.
  • RefCell<T>: runtime borrow checking for single-threaded interior mutability.
  • Mutex<T> and RwLock<T>: shared mutable state with lock discipline.

Rust Pattern

Prefer message passing when it models the system cleanly:

use std::sync::mpsc;
use std::thread;

#[derive(Debug)]
enum Event {
TaskCreated,
TaskCompleted,
}

let (tx, rx) = mpsc::channel::<Event>();

let worker = thread::spawn(move || {
let mut completed = 0;
for event in rx {
if matches!(event, Event::TaskCompleted) {
completed += 1;
}
}
completed
});

tx.send(Event::TaskCreated)?;
tx.send(Event::TaskCompleted)?;
drop(tx);

let completed = worker.join().expect("worker panicked");

The worker owns its counter. The main thread sends events. No lock is needed.

Practice

Keep this mistake out of your first implementation.

Do not wrap the whole application in shared mutable state:

type AppState = std::sync::Arc<std::sync::Mutex<Everything>>;

This design creates contention, unclear mutation boundaries, and lock-order problems.

Keep these concrete mistakes out of your work.

  • Wrapping every value in Arc<Mutex<_>>.
  • Holding locks longer than necessary.
  • Holding a lock across .await in async code.
  • Using shared mutation where channels or ownership transfer would be clearer.

Use this sequence. Do not move to the next row until you have produced the artifact in the right column.

StepFocusArtifact
BoxHeap allocation, recursive types, trait objectsRecursive command tree
Rc and RefCellSingle-threaded shared ownership and runtime borrow checkingSmall graph example
ArcThread-safe shared ownershipShared read-only config
Mutex and RwLockLock scope and mutation disciplineEvent counter
AtomicsSimple shared values and countersAtomic request counter
Threads and channelsMessage passing and ownership transferWorker thread
Send and SyncValues crossing thread/task boundariesExplanation note

Build this now. Keep each change small enough that you can run cargo check, cargo test, and inspect the diff.

Start with a design that stores Vec<Event> behind Arc<Mutex<_>>. Refactor it so:

  • producers send events through a channel,
  • one worker owns the mutable event summary,
  • the public API exposes a snapshot instead of the mutable vector,
  • tests prove concurrent sends are counted correctly.

After your own attempt, use another reviewer or an AI tool as a second pass. Accept a suggestion only when you can explain why it preserves the lesson design.

Ask AI to make an event counter thread-safe. Review whether it:

  • locks the smallest possible state,
  • avoids lock use when message passing is simpler,
  • avoids holding locks while calling user code,
  • explains why shared state is necessary.

You can move on when these statements are true.

  • Does shared mutable state earn its place?
  • Can ownership transfer replace a lock?
  • Is the lock scope minimal?
  • Could this lock be held across .await later?
  • Are Send and Sync requirements understood instead of patched around?

Curated Resources

Next Step

Continue to Async Rust and Tokio.