The one thing that blew my mind about Rust is its approach to data sharing in concurrent situations.

I had always thought of mutexes as something that is easy to get wrong and was convinced that the use of a RAII pattern to prevent lock leaks never happen (like with Abseil’s MutexLock) was the panacea. (I’m a fan of RAII in C++ by the way, in case you haven’t noticed.)

As Rust has taught me, that’s far from the truth: in Rust, you protect the data, not the code. What this means is that, e.g. a mutex is not an object to control access to a piece of data: a mutex is a container for a piece of data. It is impossible to access the data without going through the mutex.

“Ah! Not so fast! You can do exactly the same in C++ or even in Go!”, you claim. Yeeee… no, not at all. Sure, you can implement a container that holds the data and only exposes its contents via a getter after grabbing the lock. But there is nothing preventing the code from leaking a reference to that inner data after the mutex is released. Let’s imagine an interface like this (with lots of details omitted, like suppression of copies and assignments):

template< typename T >
class Lock {
    T& _data;

    friend class Mutex;
    Lock(T&);  // Grabs non-owning reference to the data.

    ~Lock();  // Releases mutex.
    const T& get() const;
    T& get();

template< typename T >
class Mutex {
    T _data;

    explicit Mutex(T&&);  // Grabs ownership of the data.
    Lock<T> lock(void);  // Acquires mutex.

Looks legit, right? We can only construct a Mutex by moving an arbitrary object into it, and from there on, the only way to get back to the object is to first call lock() on the mutex. Doing so returns us a Lock RAII construct that automatically releases the lock on destruction. And with that done, the only way to reach the data is by invoking get() on a valid Lock, which we know holds the mutex.

But, ha ha, it is trivial to escape all these nice intentions:

Mutex<SuperDuperType> mutex(std::move(data));

SuperDuperType* dangling_pointer;
    Lock<SuperDuperType> lock = mutex.lock();
    dangling_pointer = &lock.get();
}  // lock goes out of scope here so the mutex is released.

// Oh oh.  We now have a pointer to the inner object, which is not locked any
// more... and it wouldn't even be valid if the mutex had gone out of scope.

No matter how good your intentions are, such an API will leak the data in one way or another and, eventually, someone will write broken code like the one shown above—possibly along a lengthy comment explaining why they had to go through such nastiness.

So no. It’s just impossible to safely implement a mutex as a data container in C++—and, for that matter, in Go as well because the language has even fewer protections. The resulting interface would be so fragile that you may as well not bother implementing it in the first place.

In Rust, however… it is perfectly possible to implement this API safely: the Rust compiler will catch misuses and disallow code from even building when it violates data access restrictions. And that is precisely how the std::sync::Mutex type in the standard library behaves.

For completeness sake, let’s try to translate the above example to Rust:

use std::sync::Mutex;

struct SuperDuperType {
  value: i32,

fn main() {
    let data = SuperDuperType { value: 3 };
    let mutex = Mutex::new(data);

    let leaked: &SuperDuperType;
        let mut locked_data = mutex.lock().unwrap();
        locked_data.value = 5;
        leaked = &locked_data;  // Impossible.

If we try to build this code, we’ll be greeted with the following error at the point were we tried to leak the reference to the inner, locked data:

error[E0597]: `locked_data` does not live long enough
15 |         leaked = &locked_data;  // Broken.
   |                   ^^^^^^^^^^^ borrowed value does not live long enough
16 |     }
   |     - `locked_data` dropped here while still borrowed
17 | }
   | - borrowed value needs to live until here

I encourage you to try to come up with a pathological piece of code that lets you leak the reference. Try to implement it and see what happens. My original thought when I went through this exercise was: “well, what if I create a thread once the mutex is held and access the reference through the other thread? Surely that’d lead to a bug.” Turns out you cannot even express this: the reference cannot be passed to the new thread (see the move keyword for details), so you cannot reach this situation. The only thing you can pass to the new thread without losing ownership of the data is the Mutex itself or something like a thread-safe reference-counted container like an Arc. (Update: OK, I was wrong…. As Florian Gilcher points out, it’s still possible to break the safety guarantees in some cases but the good thing is that there is a Formal Verification Working Group to address these!)

The fact that the language is able to enforce these restrictions at build time is hard to believe. And these restrictions are possible thanks to Rust’s data ownership rules and thanks to our evil good friend the borrow checker, which we previously discussed.

Even with the examples above, which I know are convoluted, it’s hard for me to articulate how powerful these concepts are unless you experience their benefits firsthand. In my case, I witnessed these during the sandboxfs rewrite: while “transcribing” the original design from Go to Rust, I was able to catch a bunch of cases in which concurrency was slightly off. The code was rightfully buggy, but Go itself didn’t nor couldn’t care. On the other hand, Rust just didn’t allow me express those invalid constructions in the first place, which forced me to fix the design.

In the next episode, we’ll explore how these theoretically simple concepts tie to computing fundamentals and how they make Rust’s learning curve steep.