Lately I’ve been experimenting with Rust, and I want to report some of what I’ve learned about thread-safety. I am an enthusiastic dabbler in Rust: I spend most of my time in C and C++, but I’m always looking for an excuse to learn more about Rust’s approach to the techniques I use every day in C and C++.

When studying Rust’s threading model, I came to see some correspondence between C++ and Rust terminology that I had not seen published previously. Here are my findings, which hopefully can help people with C++ background understand Rust (or vice-versa).

C++

The C++ standard does not define the term “thread-safe”, but it is common practice now within the C++ community to define it in the following way:

  • thread-safe: A type is thread-safe if it is is safe to invoke any of its methods concurrently. To provide this guarantee, a type must generally take some special measures to avoid data races, eg. using a mutex or atomic operations internally. This generally comes with performance and/or complexity costs, so most types will not be thread-safe.
  • thread-compatible: A type is thread-compatible if it is safe to invoke const methods concurrently. Any concurrent call to a non-const method must be synchronized by the caller. Most types in C++ are thread-compatible, as this guarantee comes mostly comes for free: it happens naturally for any type that is const-correct (ie. avoids mutable members or const_cast).

Thread-compatible types compose nicely and avoid synchronization overheads. Suppose you have 10 thread-compatible objects that you want to access concurrently together. You can wrap a Mutex around all 10 and pay only a single synchronization cost. If you have 10 thread-safe objects, you pay 10 separate synchronization costs as each of them perform their own internal synchronization. If you are using an object in only one thread, you may not need synchronization at all, but the thread-safe type won’t know this and will pay the cost regardless. For all of these reasons, thread-compatible types are generally preferred.

Here is an example of a thread-safe type in C++. It achieves thread-safety by using std::atomic:

// C++ Usage of thread-safe type.
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

// A very simple thread-safe type.
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() : counter_(0) {}
  void Increment() { counter_.fetch_add(1); }
  int GetCount() const { return counter_; }

 private:
  std::atomic_int32_t counter_;
};

int main() {
  ThreadSafeCounter counter;
  const int n = 10;
  std::vector<std::thread> threads;

  // Spawn `n` threads that all share a single counter.
  for (int i = 0; i < n; i++) {
    threads.push_back(std::thread([&counter] {
      // Unsynchronized call of a non-const method.
      // Only safe because the type is thread-safe.
      counter.Increment();
    }));
  }
  for (auto& thread : threads) { thread.join(); }

  // This will ultimately print `n`.
  std::cout << counter.GetCount() << "\n";
  return 0;
}

Rust

Rust’s model for thread-safety has some notable differences. Rust’s thread-safety story centers around two traits:

  • The Sync trait indicates that a type can be safely shared between threads.
  • The Send trait indicates that a type can be safely moved between threads.

The Sync trait ends up mapping closely to the C++ concept of thread-compatible. It indicates that concurrent access of a type is safe, as long as neither of the concurrent operations operates on a mutable reference. Just as most types in C++ are thread-compatible, most types in Rust are Sync.

However, Rust and C++ differ far more when we talk about thread-safe types. Rust forbids shared mutable access at the language level. That means that the C++ way of modeling thread-safety won’t work at all in Rust. Even if we tried to make a Rust type that offered the C++ thread-safety guarantee, safe Rust code would never be able to take advantage of this guarantee, because the code would fail to compile.

For example, let’s try to port the C++ code above to Rust:

use std::thread;
use std::sync::atomic::{AtomicI32, Ordering};

struct ThreadSafeCounter {
    count: AtomicI32,
}

impl ThreadSafeCounter {
    fn increment(&mut self) { self.count.fetch_add(1, Ordering::SeqCst); }
}

pub fn main() {
    let n = 10;
    let mut counter = ThreadSafeCounter { count: AtomicI32::new(0) };
    let mut threads = Vec::new();
    for _ in 0..n {
        threads.push(thread::spawn( || {
            // Rust won't allow this.  We are attempting to mutably borrow
            // the same value multiple times.
            counter.increment();
        }));
    }
    for thread in threads { thread.join(); }
    println!("{}", counter.count.load(Ordering::SeqCst));
}

This fails to compile with:

error[E0499]: cannot borrow `counter` as mutable more than once at a time
  --> <source>:18:37
   |
18 |           threads.push(thread::spawn( || {
   |                        -              ^^ `counter` was mutably borrowed here in the previous iteration of the loop
   |  ______________________|
   | |
19 | |             // Rust won't allow this.  We are attempting to mutably borrow
20 | |             // the same value multiple times.
21 | |             counter.increment();
   | |             ------- borrows occur due to use of `counter` in closure
22 | |         }));
   | |__________- argument requires that `counter` is borrowed for `'static`

Because Rust fundamentally allows only a single mutable reference to any given object, we have to express the C++ concept of thread-safety in a different way.

The Rust answer to thread-safety is to allow mutation on an immutable reference. Rust calls this “interior mutability.” With one small change, the previous example compiles and works as expected:

use std::thread;
use std::sync::atomic::{AtomicI32, Ordering};

struct ThreadSafeCounter {
    count: AtomicI32,
}

impl ThreadSafeCounter {
    // increment() uses "interior mutability": it accepts an immutable
    // reference, but ultimately mutates the value.
    fn increment(&self) { self.count.fetch_add(1, Ordering::SeqCst); }
}

pub fn main() {
    let n = 10;
    let mut counter = ThreadSafeCounter { count: AtomicI32::new(0) };
    let mut threads = Vec::new();
    for _ in 0..n {
        threads.push(thread::spawn( || {
            counter.increment();
        }));
    }
    for thread in threads { thread.join(); }
    println!("{}", counter.count.load(Ordering::SeqCst));
}

As a C++ programmer, interior mutability strikes me as a bit of a fib: an operation that is in fact mutable, both logically and physically, is allowed on an immutable reference. This is very similar to mutable and const_cast in C++, which are both frowned on.

I found a nice explanation of the Rust perspective in this Stack Overflow answer:

In a way, Rust’s mut keyword actually has two meanings. In a pattern it means “mutable” and in a reference type it means “exclusive”. The difference between &self and &mut self is not really whether self can be mutated or not, but whether it can be aliased.

This helps explain the rationale behind interior mutability. When applied to a reference, “immutable” in Rust doesn’t really mean “immutable”, it means “non-exclusive.” This point is covered in more depth in another article, Accurate mental model for Rust’s reference types.

There was even a proposal several years back to rename &mut to &my, &only, or &uniq, to emphasize that the key property of such references is not that they are mutable, but that they are unique.

A type we would consider thread-safe in C++ will need to use interior mutability in Rust and implement the Sync trait. It will need to allow mutating operations to be performed through an immutable reference.

This difference is reflected in the API of the atomics we were using above:

  • In C++, std::atomic_int32_t::fetch_add() is a non-const operation. This makes sense, as the operation does in fact mutate the atomic. Callers have to read the documentation to know that concurrent calls to this non-const method are safe.
  • In Rust, std::sync::atomic::AtomicI32::fetch_add() is an immutable operation (takes a non-mut reference). This is the interior mutability “fib” (as fetch_add() will in fact mutate the atomic), but it has the benefit of expressing the the type’s thread-safety guarantee within the type system, which allows the compiler to automatically check it.

Rust has the obvious advantage of having thread-safety modeled within the type system, and checked by the compiler. Rust even allows a single type to have both thread-safe and non-thread-safe methods. An example is std::sync::Mutex, which provides both Mutex::lock(&self), a thread-safe method that locks the mutex, and Mutex::get_mut(&mut self), which allows access to the mutex’s data without any synchronization costs. If the caller holds a unique reference, synchronization is unnecessary, and Rust lets us avoid this overhead. This is all modeled within the type system and checked automatically.

Mapping between C++ and Rust terminology

The analysis above leaves us with the following mapping between terms:

C++ Rust Example
thread-compatible implements Sync most types (eg. Vec or vector)
thread-safe implements Sync with interior mutability AtomicI32
thread-unsafe doesn’t implement Sync Cell, RefCell in Rust

We can also put things in quadrants like so:

Sync !Sync
interior mutability thread-safe (AtomicI32) thread-unsafe (Cell)
no interior mutability thread-compatible (Vec) thread-unsafe (proc_macro)

The two quadrants under !Sync are both labeled “thread-unsafe.” The C++ terminology does not distinguish between these two cases, and neither does the type system. In Rust there are interesting differences between them. With interior mutability come types like Cell and RefCell that can provide safe interior mutability by constraining the circumstances under which mutation can occur. The “no interior mutability” case here initially seems not useful, but as pointed out to me on lobste.rs, it can actually be quite useful when combined with !Send, as it allows one to create a handle to thread-local data that can only be safely used within a single thread.


Thanks to Matt Brubeck for reading a draft version of this article. Matt has an article that delves more deeply into unique vs shared references.