No, it is still a problem.
- Writes are not always guaranteed to be atomic. For example on a 32-bit system, a
u64
takes multiple cpu cycles to write - and therefore the reading side could see only half of the value updated.
- This breaks soundness because the compiler can no longer prove that your code is free of undefined behavior
It is true that accessing very simple primitive types like this can be safe. You don't need static mut
for it, though - there are mechanisms built into the language / core library so you don't have to resort to static mut
. In this case, the important one would be atomic.
It provides something called interior mutability. This means your value can be static
without mut
, and can be shared normally, and the type itself provides the mutability.
Let me demonstrate. As I don't have a microcontroller available right now, I rewrote your example for normal execution:
use core::time::Duration;
static mut ONE_WAY_DATA_EXCHANGE: u8 = 0;
fn main() {
std::thread::scope(|s| {
s.spawn(thread0);
s.spawn(thread1);
});
}
fn thread0() {
std::thread::sleep(Duration::from_millis(500));
unsafe { ONE_WAY_DATA_EXCHANGE = 42 };
}
fn thread1() {
for _ in 0..4 {
std::thread::sleep(Duration::from_millis(200));
let value = unsafe { ONE_WAY_DATA_EXCHANGE };
println!("{}", value);
}
}
0
0
42
42
Here is how this would look like when implemented with atomic
:
use core::{
sync::atomic::{AtomicU8, Ordering},
time::Duration,
};
static ONE_WAY_DATA_EXCHANGE: AtomicU8 = AtomicU8::new(0);
fn main() {
std::thread::scope(|s| {
s.spawn(thread0);
s.spawn(thread1);
});
}
fn thread0() {
std::thread::sleep(Duration::from_millis(500));
ONE_WAY_DATA_EXCHANGE.store(42, Ordering::Release);
}
fn thread1() {
for _ in 0..4 {
std::thread::sleep(Duration::from_millis(200));
let value = ONE_WAY_DATA_EXCHANGE.load(Ordering::Acquire);
println!("{}", value);
}
}
0
0
42
42
Note that the code does not contain an unsafe
; this is prefectly valid for the compiler to understand and has (almost) no runtime overhead.
To demonstrate how little overhead this really causes:
#![no_std]
use core::sync::atomic::{AtomicU8, Ordering};
static SHARED_VALUE_ATOMIC: AtomicU8 = AtomicU8::new(0);
pub fn write_static_atomic(val: u8){
SHARED_VALUE_ATOMIC.store(val, Ordering::SeqCst)
}
pub fn read_static_atomic() -> u8 {
SHARED_VALUE_ATOMIC.load(Ordering::SeqCst)
}
static mut SHARED_VALUE_STATICMUT: u8 = 0;
pub fn write_static_staticmut(val: u8){
unsafe {
SHARED_VALUE_STATICMUT = val;
}
}
pub fn read_static_staticmut() -> u8 {
unsafe {
SHARED_VALUE_STATICMUT
}
}
The code compiles to the following, using the flags -C opt-level=3 -C linker-plugin-lto --target=thumbv6m-none-eabi
:
example::write_static_atomic:
dmb sy
ldr r1, .LCPI0_0
strb r0, [r1]
dmb sy
bx lr
.LCPI0_0:
.long example::SHARED_VALUE_ATOMIC.0
example::read_static_atomic:
ldr r0, .LCPI1_0
ldrb r0, [r0]
dmb sy
bx lr
.LCPI1_0:
.long example::SHARED_VALUE_ATOMIC.0
example::write_static_staticmut:
ldr r1, .LCPI2_0
strb r0, [r1]
bx lr
.LCPI2_0:
.long example::SHARED_VALUE_STATICMUT.0
example::read_static_staticmut:
ldr r0, .LCPI3_0
ldrb r0, [r0]
bx lr
.LCPI3_0:
.long example::SHARED_VALUE_STATICMUT.0
example::SHARED_VALUE_ATOMIC.0:
.byte 0
example::SHARED_VALUE_STATICMUT.0:
.byte 0
An AtomicU8
in on thumbv6m-none-eabi
seems to have almost zero overhead. The only changes are the dmb sy
, which are memory barriers that prevent race conditions; using Ordering::Relaxed
(if your problem allows it) should eliminate those, causing actual zero overhead. Other architectures should behave similar.