Write about acquire and release fences

pull/378/head
SabrinaJewson 3 years ago
parent 3c76e35449
commit d4f8f47439
No known key found for this signature in database
GPG Key ID: 3D5438FFA5F05564

@ -1 +1,202 @@
# Fences
As well as loads, stores, and RMWs, there is one more kind of atomic operation
to be aware of: fences. Fences can be triggered by the
[`core::sync::atomic::fence`] function, which accepts a single ordering
parameter and returns nothing. They dont do anything on their own, but can be
thought of as events that strengthen the ordering of nearby atomic operations.
## Acquire fences
The most common kind of fence is an _acquire fence_, which can be triggered in
three different ways:
1. `atomic::fence(atomic::Ordering::Acquire)`
1. `atomic::fence(atomic::Ordering::AcqRel)`
1. `atomic::fence(atomic::Ordering::SeqCst)`
An acquire fence retroactively makes every single non-`Acquire` operation that
was sequenced-before it act like an `Acquire` operation that occurred at the
fence — in other words, it causes every prior `Release`d value that was
previously loaded on the thread to synchronize-with the fence. For example, the
following code:
```rust
# use std::sync::atomic::{self, AtomicU32};
static X: AtomicU32 = AtomicU32::new(0);
// t_1
X.store(1, atomic::Ordering::Release);
// t_2
let value = X.load(atomic::Ordering::Relaxed);
atomic::fence(atomic::Ordering::Acquire);
```
Can result in two possible executions:
```text
Possible Execution 1 ┃ Possible Execution 2
t_1 X t_2 ┃ t_1 X t_2
╭───────╮ ┌───┐ ╭───────╮ ┃ ╭───────╮ ┌───┐ ╭───────╮
│ store ├─┐ │ 0 │ ┌─┤ load │ ┃ │ store ├─┐ │ 0 ├───┤ load │
╰───────╯ │ └───┘ │ ╰───╥───╯ ┃ ╰───────╯ │ └───┘ ╰───╥───╯
└─↘───┐ │ ╭───⇓───╮ ┃ └─↘───┐ ╭───⇓───╮
│ 1 ├─┘┌→ fence │ ┃ │ 1 │ │ fence │
└───┴──┘╰───────╯ ┃ └───┘ ╰───────╯
```
In the first execution, `t_1`s store synchronizes-with and therefore
happens-before `t_2`s fence due to the prior load, but note that it does _not_
happen-before `t_2`s load.
Acquire fences work on any number of atomics, and on release sequences too. A
more complex example is as follows:
```rust
# use std::sync::atomic::{self, AtomicU32};
static X: AtomicU32 = AtomicU32::new(0);
static Y: AtomicU32 = AtomicU32::new(0);
// t_1
X.store(1, atomic::Ordering::Release);
X.fetch_add(1, atomic::Ordering::Relaxed);
// t_2
Y.store(1, atomic::Ordering::Release);
// t_3
let x = X.load(atomic::Ordering::Relaxed);
let y = Y.load(atomic::Ordering::Relaxed);
atomic::fence(atomic::Ordering::Acquire);
```
This can result in an execution like so:
```
t_1 X t_3 Y t_2
╭───────╮ ┌───┐ ╭───────╮ ┌───┐ ╭───────╮
│ store ├─┐ │ 0 │ ┌─┤ load │ │ 0 │ ┌─┤ store │
╰───╥───╯ │ └───┘ │ ╰───╥───╯ └───┘ │ ╰───────╯
╭───⇓───╮ └─↘───┐ │ ╭───⇓───╮ ┌───↙─┘
│ rmw ├─┐ │ 1 │ │ │ load ├───┤ 1 │
╰───────╯ │ └─┬─┘ │ ╰───╥───╯ ┌─┴───┘
└─┬─↓─┐ │ ╭───⇓───╮ │
│ 2 ├─┘┌→ fence ←─┘
└───┴──┘╰───────╯
```
There are two common scenarios in which acquire fences are used:
1. When an `Acquire` ordering is only necessary when a specific value is loaded.
For example, you may only wish to acquire when an `initialized` boolean is
`true`, since otherwise you wont be reading the shared state at all. In
this case, you can load with a `Relaxed` ordering and then issue an
`Acquire` fence afterward only if that condition is met, which can aid in
performance sometimes (since the acquire operation is avoided when
`initialized == false`).
2. When several `Acquire` operations on different locations need to be performed
in a row, but individually each operation doesnt need `Acquire` ordering;
it is often faster to perform all the loads as `Relaxed` first and use a
single `Acquire` fence at the end then it is to make each one separately use
`Acquire`.
## Release fences
Release fences are the natural complement to acquire fences, and they similarly
can be triggered in three different ways:
1. `atomic::fence(atomic::Ordering::Release)`
1. `atomic::fence(atomic::Ordering::AcqRel)`
1. `atomic::fence(atomic::Ordering::SeqCst)`
Release fences convert every subsequent atomic access in the same thread into a
release operation that has its arrow starting from the fence — in other words,
every `Acquire` operation that sees a value that was written by the fences
thread after the release fence will synchronize-with the release fence. For
example, the following code:
```rust
# use std::sync::atomic::{self, AtomicU32};
static X: AtomicU32 = AtomicU32::new(0);
// t_1
atomic::fence(atomic::Ordering::Release);
X.store(1, atomic::Ordering::Relaxed);
// t_2
X.load(atomic::Ordering::Acquire);
```
Can result in this execution:
```text
t_1 X t_2
╭───────╮ ┌───┐ ╭───────╮
│ fence ├─┐ │ 0 │ ┌─→ load │
╰───╥───╯ │ └───┘ │ ╰───────╯
╭───⇓───╮ └─↘───┐ │
│ store ├───┤ 1 ├─┘
╰───────╯ └───┘
```
As well as it being possible for a release fence to synchronize-with an acquire
load (fenceatomic synchronization) and a release store to synchronize-with an
acquire fence (atomicfence synchronization), it is also possible for release
fences to synchronize with acquire fences (fencefence synchronization). In this
code snippet, only fences and `Relaxed` operations are used to establish a
happens-before relation (in some executions):
```rust
# use std::sync::atomic::{self, AtomicU32};
static X: AtomicU32 = AtomicU32::new(0);
// t_1
atomic::fence(atomic::Ordering::Release);
X.store(1, atomic::Ordering::Relaxed);
// t_2
X.load(atomic::Ordering::Relaxed);
atomic::fence(atomic::Ordering::Acquire);
```
The execution with the relation looks like this:
```text
t_1 X t_2
╭───────╮ ┌───┐ ╭───────╮
│ fence ├─┐ │ 0 │ ┌─┤ load │
╰───╥───╯ │ └───┘ │ ╰───╥───╯
╭───⇓───╮ └─↘───┐ │ ╭───⇓───╮
│ store ├───┤ 1 ├─┘┌→ fence │
╰───────╯ └───┴──┘╰───────╯
```
Like with acquire fences, release fences are commonly used to optimize over a
series of atomic stores that dont individually need to be `Release`, since its
often faster to put a single release fence at the start and use `Relaxed` from
that point on than it is to use `Release` every time.
## `AcqRel` fences
`AcqRel` fences are just the combined behaviour of an `Acquire` fence and a
`Release` fence in one operation. There isnt much special to note about them,
other than that they behave more like an acquire fence followed by a release
fence than the other way around, which is useful to know in situations like the
following:
```text
t_1 X t_2 Y t_3
╭───────╮ ┌───┐ ╭───────╮ ┌───┐ ╭───────╮
│ A │ │ 0 │ ┌─┤ load │ │ 0 │ ┌─→ load │
╰───╥───╯ └───┘ │ ╰───╥───╯ └───┘ │ ╰───╥───╯
╭───⇓───╮ ┌─↘───┐ │ ╭───⇓───╮┌──↘───┐ │ ╭───⇓───╮
│ store ├─┘ │ 1 ├─┘┌→ fence ├┘┌─┤ 1 ├─┘ │ B │
╰───────╯ └───┴──┘╰───╥───╯ │ └───┘ ╰───────╯
╭───⇓───╮ │
│ store ├─┘
╰───────╯
```
Here, A happens-before B, which is singularly due to the `AcqRel` fences
ability to “carry over” happens-before relations within itself.
[`core::sync::atomic::fence`]: https://doc.rust-lang.org/stable/core/sync/atomic/fn.fence.html

Loading…
Cancel
Save