I guess the question is then about the meaning of the word safe, or safety. In the context of Rust, this has a quite narrow scope, perharps surprisingly narrow, whereby the safe vs. unsafe
dichotomy is about memory safety or, equivalently, defined behavior (or lack thereof: UB).
Some examples:
-
::std::fs::remove_dir_all("/")
is a non-unsafe
operation in Rust, which could thus be labelled as safe, as in, (process-)memory-safe. Even though it's attempting to nuke all your data on permanent storage
-
struct Troll();
impl Hash for Troll { … random() }
let instance = Troll();
let mut set = HashSet::new();
set.insert(&instance);
assert!(set.contains(&instance)); // will probably fail!
Here we have an example of API misusage and logic bugs due to a memory state that does not match the program(mer)'s expectation. It's still memory-safe, in Rust parlance.
So, back to concurrency vs. parallelism, and in the context of:
Let's take an example: incrementing a number concurrently.
/// `::syn` can't parse this, btw…
use {
::core::{
cell::Cell as Mut,
},
::futures::{
executor,
future::join,
},
};
fn main ()
{
let state: Mut<i32> = 0.into();
let task = || async {
let value = state.get();
let () = stuff().await;
state.set(value + 1);
};
// You can write Rust and draw faces at once 🙃
((..) , (..)) = executor::block_on(join(task(), task()));
assert_eq!(state.get(), 2);
}
This, runs that task
twice, concurrently. Thus, depending on the behavior of stuff()
and executor::block_on
, the assertion will fail or pass.
So we have a "re-entrancy bug", we could say (depends on the point of view, to be honest), albeit a memory-safe one: no memory was harmed unsafe
was written for this demo.
- If
stuff()
were to yield based on some timer, for instance, directly or indirectly, then this bug could be labelled under the race condition category, but it would nonetheless not be a data race, in the hardware sense at least.
Now, if instead of the above we were to write, using some unsafe
, the following:
fn main ()
{
static mut STATE: i32 = 0;
let task = || unsafe {
let state = ::core::ptr::addr_of_mut!(STATE);
// same as `*state += 1;`
let value = *state; *state = value + 1;
};
((), ()) = ::rayon::join(task, task); // UB!
}
whereby we are still featuring concurrency through join
, but this time the single-threaded (and thus, parallelism-free) ::futures::executor::block_on(join(…))
has been replaced with the allowed-to-be-run-in-parallel ::rayon::join
.
This means that while a thread is write-accessing *state
in *state = …
, the other thread may be accessing that same *state
as well, either for reading or for writing. This is the textbook example of a data race, and it can lead to memory unsafety:
value
is no longer guaranteed to be 0
or 1
, it could be any arbitrary bit pattern (this is quite concerning given that value
could have been typed, in Rust, as a bool
, which makes observing a bit-pattern that is neither 0
nor 1
already UB), and the same applies to *state
, i.e., to STATE
.
Now, in order to simplify this example of UB, I have used a static mut
, so that the closures are officially not capturing anything, and thus get to be Send
, letting that example compile fine
. But this is actually rather illustrating how dangerous static mut
can be, even if I've taken the care of using addr_of_mut!
rather than &mut
to avoid other aliasing concerns that static mut
has (and which would otherwise have been a source of UB before the data race even occurred).
But if we were to go back to my cell::Mut
example, so as not to use unsafe
:
use ::core::cell::Cell as Mut;
fn main ()
{
let state: Mut<i32> = 0.into();
let task = || {
let value = state.get();
state.set(value + 1);
};
((..) , (..)) = ::rayon::join(task, task); // UB!
}
- (modulo the stack vs. global storage distinction for
state
, this snippet would have the exact semantics w.r.t. what the task
s are doing)
this fails to compile , with:
error[E0277]: `Cell<i32>` cannot be shared between threads safely
--> src/main.rs:10:21
|
10 | ((..) , (..)) = ::rayon::join(task, task); // UB!
| ^^^^^^^^^^^^^ `Cell<i32>` cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `Cell<i32>`
= note: required because of the requirements on the impl of `Send` for `&Cell<i32>`
= note: required because it appears within the type `[closure@src/main.rs:6:16: 9:6]`
note: required by a bound in `rayon::join`
So there we have it. A comparison of single-threaded and parallel concurrency w.r.t. incrementing a shared counter, which is as basic as a shared data structure can get
We can see how Cell<i32>
is a concurrently-mutable integer, which is nevertheless (memory-)unsafe to mutate in parallel.