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 tasks 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.