|
|
|
|
# Relaxed
|
|
|
|
|
|
|
|
|
|
Now we’ve got single-threaded mutation semantics out of the way, we can try
|
|
|
|
|
reintroducing a second thread. We’ll have one thread perform a write to the
|
|
|
|
|
memory location, and a second thread read from it, like so:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
// Initial state
|
|
|
|
|
let mut data = 0;
|
|
|
|
|
// Thread 1:
|
|
|
|
|
data = 1;
|
|
|
|
|
// Thread 2:
|
|
|
|
|
println!("{data}");
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Of course, any Rust programmer will immediately tell you that this code doesn’t
|
|
|
|
|
compile, and indeed it definitely does not, and for good reason. But suspend
|
|
|
|
|
your disbelief for a moment, and imagine what would happen if it did. Let’s draw
|
|
|
|
|
a diagram, leaving out the reading lines for now:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Thread 1 data Thread 2
|
|
|
|
|
╭───────╮ ┌────┐ ╭───────╮
|
|
|
|
|
│ = 1 ├╌┐ │ 0 │ ?╌┤ data │
|
|
|
|
|
╰───────╯ ├╌┼╌╌╌╌┤ ╰───────╯
|
|
|
|
|
└╌┼╌╌╌╌┤
|
|
|
|
|
│ 1 │
|
|
|
|
|
└────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Let’s try to figure out where the line in Thread 2’s access joins up. The rules
|
|
|
|
|
from before don’t help us much unfortunately since there are no arrows
|
|
|
|
|
connecting that operation to anything, so we can’t immediately rule anything
|
|
|
|
|
out. As a result, we end up facing a situation we haven’t faced before: there is
|
|
|
|
|
_more than one_ potential value for Thread 2 to read.
|
|
|
|
|
|
|
|
|
|
And this is where we encounter the big limitation with unsynchronized data
|
|
|
|
|
accesses: the price we pay for their speed and optimization capability is that
|
|
|
|
|
this situation is considered **Undefined Behavior**. For an unsynchronized read
|
|
|
|
|
to be acceptable, there has to be _exactly one_ potential value for it to read,
|
|
|
|
|
and when there are multiple like in this situation it is considered a data race.
|
|
|
|
|
|
|
|
|
|
## “Out-of-thin-air” values
|