Why is it "unsafe" to share memory in a single threaded application?

Tell me if i'm wrong, but isn't the memory safety of rust there so that data races cannot occur? If data races require 2+ threads to be accessing the same memory, then why is it "unsafe" to share memory in a single threaded application?

Exclusive mutable access in Rust, as I understand it is, is not primarily to address race conditions, but to enforce invariants that ensure memory safety.

Consider this code: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a7651b673c87a882abf7f149ebb70949

If you don't know, the way a Vec is implemented is that it will tend to overallocate memory. As more elements get pushed into the Vec, if the allocation is exceeded, it will reallocate a new buffer and discard the old one. Of course, this invalidates any existing references to the vector.

If Rust allowed borrow mutable access while still allowing mutable access through my_vec, then any reallocation of my_vec from push would invalidate borrow, in which case you would violate memory safety.

5 Likes

If one variable holds a pointer to a value, and accessing another variable can invalidate that pointer (by freeing the value, moving it, or changing its type), then you have a memory safety violation. This can happen even if both variables are accessed on a single thread.

For several concrete examples, see The Problem With Single-Threaded Shared Mutability.

7 Likes

I like to think of it like this...

Even in single threaded code one has activity going on that is not immediately apparent in ones source text. A sort of "thread" acting behind the scenes that you don't immediately see.

The classic example is the Vec as told by AndrewGaspar above. You might think that you have a vector and a bunch of references into it's elements and everything is OK. In reality, behind the scenes that vector is managed by the code that implements vector, it can reallocate and move it's data around as it sees fit. Or deallocate it altogether. Then all your references are invalid.

Or what about returning a reference to some data that is local to a function? The caller gets the reference back but it's invalid as the data was created on the stack and ceased to exist on return from the function.

All these things are hard for a programmer to keep track of. Which is why C/C++ and many others are famous for such problems. Which can be hard to debug.

I am very happy to finally have a language and compiler that does all that bookkeeping "busy work" for me.

2 Likes

To expand on this a bit for those familiar with C++:

Just about every C++ bug involving "iterator invalidation", "dangling references", "dangling lambda captures", "string_view invalidation" and so on in a single-threaded program is an example of exactly this issue, and the equivalent (safe) Rust code would simply not pass the borrow checker.

5 Likes