Post

Rust Notes: Concurrency, Threads, and Thread Safety

Learning notes about concurrency, parallelism, Rust threads, Send, Sync, and communication models.

Rust Notes: Concurrency, Threads, and Thread Safety

Rust Notes: Concurrency, Threads, and Thread Safety

Mental model:

  • Concurrency: multiple tasks make progress independently.
  • Parallelism: multiple tasks execute at the same time.
  • Send: ownership can move between threads.
  • Sync: references can be safely shared between threads.

Concurrency vs Parallelism

Concurrency

Concurrency means that different parts of a program can make progress independently.

Example:

1
2
3
Task A: ========

Task B:     ========

The tasks may be executed:

  • interleaved on a single CPU core,
  • or actually in parallel on multiple cores.

Examples:

  • handling multiple network connections,
  • responding to user input while performing background work.

Parallelism

Parallelism means that different parts of a program execute at the same time.

Example:

1
2
3
CPU 1: Task A ========

CPU 2: Task B ========

Parallelism requires multiple execution units, usually multiple CPU cores.

Examples:

  • image processing,
  • scientific computation,
  • large data processing.

Threads

A thread is an independent execution path inside a process.

A process can contain multiple threads:

1
2
3
4
5
6
7
Process
 |
 +-- Thread 1
 |
 +-- Thread 2
 |
 +-- Thread 3

Threads share:

  • memory,
  • address space,
  • resources.

Sharing memory makes communication fast, but it can introduce problems such as data races.


Data Races

A data race happens when:

  • multiple threads access the same memory,
  • at least one access is a write,
  • there is no synchronization.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
Initial value:

counter = 0


Thread A              Thread B

read counter          read counter

+1                    +1

write 1               write 1

Expected:

1
counter = 2

Actual:

1
counter = 1

Rust prevents these problems using:

  • ownership,
  • borrowing rules,
  • Send,
  • Sync.

Thread Models

Programming languages use different models for creating threads.


1:1 Thread Model

Rust standard library uses the 1:1 threading model.

Meaning:

1
2
3
4
5
Language Thread
       |
       |
       v
Operating System Thread

Each language thread corresponds to one OS thread.

Example:

1
2
3
4
5
use std::thread;

thread::spawn(|| {
    println!("hello");
});

Rust calls operating system APIs to create the thread.

Advantages

  • Simple model.
  • Uses the operating system scheduler.
  • Predictable performance.
  • No large runtime required.

Disadvantages

  • OS threads are relatively expensive.
  • Creating millions of threads is not practical.

Green Threads

Some languages provide threads managed by the language runtime.

These are called:

  • green threads,
  • user-level threads,
  • lightweight threads.

The model is:

1
2
3
4
5
6
7
8
9
10
11
Many Language Threads

          |
          v

   Language Runtime

          |
          v

     OS Threads

The runtime is responsible for scheduling these threads.

Advantages

  • Very lightweight.
  • Can create many more threads.
  • Good for highly concurrent applications.

Disadvantages

  • Requires a runtime scheduler.
  • Adds complexity to the language runtime.

M:N Thread Model

The M:N model maps many language threads to fewer OS threads.

1
2
3
4
5
6
7
8
9
10
11
M Language Threads

        |
        v

Runtime Scheduler

        |
        v

N OS Threads

Example:

1
2
3
4
5
6
7
8
9
10
11
10000 application tasks

          |
          v

Runtime Scheduler

          |
          v

8 OS Threads

The runtime decides where tasks execute.

Rust’s standard library does not provide M:N green threads.

The standard std::thread API uses the 1:1 model.


Why Rust Uses 1:1 Threads

Rust focuses on:

  • zero-cost abstractions,
  • predictable performance,
  • minimal runtime.

Instead of putting a thread scheduler into the language, Rust allows libraries to provide higher-level concurrency models.

Examples:

  • async runtimes,
  • Tokio.

Send Trait

Send is a marker trait that means:

Ownership of a value can safely move to another thread.

Example:

1
2
3
4
5
6
7
use std::thread;

let value = String::from("hello");

thread::spawn(move || {
    println!("{}", value);
});

The ownership moves:

1
2
3
4
5
6
7
8
9
10
11
Thread A

String

  |
  | move
  v

Thread B

String

The compiler checks:

1
String: Send

Sync Trait

Sync is a marker trait that means:

A type can safely be shared between threads through immutable references (&T).

Example:

1
2
3
4
5
6
7
8
9
Thread A
    |
   &String
    |
    String
    |
   &String
    |
Thread B

Multiple threads can read the same value safely.

Therefore:

1
String: Sync

The formal definition:

1
T is Sync if &T is Send

Meaning:

If a reference to T can safely move to another thread, then T is safe to share.


Send vs Sync

Send

Question:

Can ownership move to another thread?

1
2
3
4
5
6
7
8
9
10
Thread A

    T

    |
    v

Thread B

    T

Requires:

1
T: Send

Sync

Question:

Can multiple threads safely have &T?

1
2
3
4
5
Thread A ----\
              \
               &T
              /
Thread B ----/

Requires:

1
T: Sync

Example: String

String is both Send and Sync.

Send

Move ownership:

1
2
3
thread::spawn(move || {
    println!("{}", s);
});

The String moves to another thread.


Sync

Share references:

1
2
3
4
5
6
7
8
9
thread::scope(|scope| {
    scope.spawn(|| {
        println!("{}", s);
    });

    scope.spawn(|| {
        println!("{}", s);
    });
});

The closures capture references:

1
&String

The compiler checks that:

1
String: Sync

Example: Rc vs Arc

Rc

Rc<T> uses a normal reference counter.

Multiple threads updating the counter could create a data race.

Therefore:

1
2
Rc<T>: !Send
Rc<T>: !Sync

Arc

Arc<T> uses atomic reference counting.

It is designed for multi-threaded shared ownership.

1
2
3
4
5
6
7
Thread A ----\
              \
               Arc
                |
                T
              /
Thread B ----/

Therefore:

1
2
Arc<T>: Send
Arc<T>: Sync

if T is thread safe.


Shared Memory vs Message Passing

A common concurrency principle:

“Do not communicate by sharing memory; instead, share memory by communicating.”


Shared Memory

Example:

1
Arc<Mutex<T>>

Model:

1
2
3
4
5
6
7
Thread A ----\
              \
              Mutex
                |
                T
              /
Thread B ----/

The mutex ensures only one thread can mutate the data at a time.

Advantages:

  • Direct access to data.
  • Sometimes better performance.

Disadvantages:

  • Locking complexity.
  • Possible deadlocks.
  • Harder reasoning.

Message Passing

Instead of sharing memory, threads communicate by sending messages.

Example:

1
2
3
use std::sync::mpsc;

let (sender, receiver) = mpsc::channel();

Sender:

1
sender.send(value);

Receiver:

1
receiver.recv();

Ownership moves through the channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread A

   value

     |
     v

 Channel

     |
     v

Thread B

Advantages:

  • Easier reasoning.
  • Ownership prevents many races.

Disadvantages:

  • Message passing can have overhead.

Common Thread Safety Rules

TypeSendSyncReason
String✅✅Safe ownership transfer and shared reading
Vec<T>✅✅If T is safe
Box<T>✅✅If T is safe
Rc<T>❌❌Non-atomic reference counter
Arc<T>✅✅Atomic reference counter
RefCell<T>depends❌Runtime borrowing is not thread safe
Mutex<T>✅✅Synchronizes access

Rust Concurrency Philosophy

Rust does not prevent concurrency.

Rust prevents unsafe concurrency.

The compiler checks:

  • Can this value move between threads? → Send
  • Can references to this value be shared between threads? → Sync

If the answer is yes, the program compiles.

If not, Rust rejects the program before runtime.


Key Takeaways

  • Concurrency means independent progress.
  • Parallelism means simultaneous execution.
  • Rust standard threads use the 1:1 OS thread model.
  • Green threads require a runtime scheduler.
  • Rust avoids built-in green threads to keep the language lightweight.
  • Send allows ownership transfer between threads.
  • Sync allows safe sharing through immutable references.
  • Shared memory requires synchronization.
  • Message passing avoids many shared-state problems.
  • Rust uses ownership rules to make concurrent programming safer.
This post is licensed under CC BY 4.0 by the author.