Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/doc/unstable-book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- [collections](collections.md)
- [collections_range](collections-range.md)
- [command_envs](command-envs.md)
- [compiler_barriers](compiler-barriers.md)
- [compiler_builtins](compiler-builtins.md)
- [compiler_builtins_lib](compiler-builtins-lib.md)
- [concat_idents](concat-idents.md)
Expand Down
111 changes: 111 additions & 0 deletions src/doc/unstable-book/src/compiler-barriers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# `compiler_barriers`

The tracking issue for this feature is: [#41091]

[#41091]: https://github.com/rust-lang/rust/issues/41091

------------------------

The `compiler_barriers` feature exposes the `compiler_barrier` function
in `std::sync::atomic`. This function is conceptually similar to C++'s
`atomic_signal_fence`, which can currently only be accessed in nightly
Rust using the `atomic_singlethreadfence_*` instrinsic functions in
`core`, or through the mostly equivalent literal assembly:

```rust
#![feature(asm)]
unsafe { asm!("" ::: "memory" : "volatile") };
```

A `compiler_barrier` restricts the kinds of memory re-ordering the
compiler is allowed to do. Specifically, depending on the given ordering
semantics, the compiler may be disallowed from moving reads or writes
from before or after the call to the other side of the call to
`compiler_barrier`. Note that it does **not** prevent the *hardware*
from doing such re-orderings -- for that, the `volatile_*` class of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volatile doesn't help in that regard, it's just another form of compiler barrier really.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, you're right. I've modified the text locally, but waiting to push until we settle on the things below.

functions, or full memory fences, need to be used.

## Examples

`compiler_barrier` is generally only useful for preventing a thread from
racing *with itself*. That is, if a given thread is executing one piece
of code, and is then interrupted, and starts executing code elsewhere
(while still in the same thread, and conceptually still on the same
core). In traditional programs, this can only occur when a signal
handler is registered. In more low-level code, such situations can also
arise when handling interrupts, when implementing green threads with
pre-emption, etc.

To give a straightforward example of when a `compiler_barrier` is
necessary, consider the following example:

```rust
# use std::sync::atomic::{AtomicBool, AtomicUsize};
# use std::sync::atomic::{ATOMIC_BOOL_INIT, ATOMIC_USIZE_INIT};
# use std::sync::atomic::Ordering;
static IMPORTANT_VARIABLE: AtomicUsize = ATOMIC_USIZE_INIT;
static IS_READY: AtomicBool = ATOMIC_BOOL_INIT;

fn main() {
IMPORTANT_VARIABLE.store(42, Ordering::Relaxed);
IS_READY.store(true, Ordering::Relaxed);
}

fn signal_handler() {
if IS_READY.load(Ordering::Relaxed) {
assert_eq!(IMPORTANT_VARIABLE.load(Ordering::Relaxed), 42);
}
}
```

The way it is currently written, the `assert_eq!` is *not* guaranteed to
succeed, despite everything happening in a single thread. To see why,
remember that the compiler is free to swap the stores to
`IMPORTANT_VARIABLE` and `IS_READ` since they are both
`Ordering::Relaxed`. If it does, and the signal handler is invoked right
after `IS_READY` is updated, then the signal handler will see
`IS_READY=1`, but `IMPORTANT_VARIABLE=0`.

Using a `compiler_barrier`, we can remedy this situation:

```rust
#![feature(compiler_barriers)]
# use std::sync::atomic::{AtomicBool, AtomicUsize};
# use std::sync::atomic::{ATOMIC_BOOL_INIT, ATOMIC_USIZE_INIT};
# use std::sync::atomic::Ordering;
use std::sync::atomic::compiler_barrier;

static IMPORTANT_VARIABLE: AtomicUsize = ATOMIC_USIZE_INIT;
static IS_READY: AtomicBool = ATOMIC_BOOL_INIT;

fn main() {
IMPORTANT_VARIABLE.store(42, Ordering::Relaxed);
// prevent earlier writes from being moved beyond this point
compiler_barrier(Ordering::Release);
IS_READY.store(true, Ordering::Relaxed);
}

fn signal_handler() {
if IS_READY.load(Ordering::Relaxed) {
assert_eq!(IMPORTANT_VARIABLE.load(Ordering::Relaxed), 42);
}
}
```

In more advanced cases (for example, if `IMPORTANT_VARIABLE` was an
`AtomicPtr` that starts as `NULL`), it may also be unsafe for the
compiler to hoist code using `IMPORTANT_VARIABLE` above the
`IS_READY.load`. In that case, a `compiler_barrier(Ordering::Acquire)`
should be placed at the top of the `if` to prevent this optimizations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you need second compiler barrier regardless of the type. Also IMPORTANT_VARIABLE doesn't need to be atomic only the variable that you are synchronzing on (IS_READY).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, come to think of it, I'm not sure either variable needs to be atomic? It's convenient because Rust doesn't easily have mutable statics otherwise, but this should also work correctly even if they're both "normal" memory locations.

As for the need for the second compiler barrier, I'm not sure I see why? It's fine for the load of IMPORTANT_VARIABLE to occur before the load of IS_READY as long as IMPORTANT_VARIABLE isn't used (which it wouldn't be if IS_READY=0), no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IS_READY needs to be atomic otherwise it could be partially written by main leaving it in some indeterminate state when the signal handler runs and tries to read it.

Yes it's fine if it isn't used, but what if it is used when IS_READY == 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine for it to be used if IS_READY == 1 — the write to IMPORTANT_VARIABLE must have succeeded, so it should contain the right value. Even if you read it before you read IS_READY.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I see what you mean. I was thinking in the more general case where the context could switch back again between the two. Something like an RTOS on a single core, but in that case you should just use a memory fence instead because they will basically be free on single core, and when you go to multi core your code won't break.

For a signal handler like this the whole thing is effectively atomic with respect to thread1 which also means the load of IS_READY doesn't need to be atomic either, only the store does.

I'm not sure what you mean about the pointer though. The compiler can't hoist out the pointer dereference unless it already know it's a valid pointer, otherwise just normal single threaded code would break, wouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, of course, you're right. I'll remove the point about pointers.


A deeper discussion of compiler barriers with various re-ordering
semantics (such as `Ordering::SeqCst`) is beyond the scope of this text.
Curious readers are encouraged to read the Linux kernel's discussion of
[memory barriers][1], the C++ references on [`std::memory_order`][2] and
[`atomic_signal_fence`][3], and [this StackOverflow answer][4] for
further details.

[1]: https://www.kernel.org/doc/Documentation/memory-barriers.txt
[2]: http://en.cppreference.com/w/cpp/atomic/memory_order
[3]: http://www.cplusplus.com/reference/atomic/atomic_signal_fence/
[4]: http://stackoverflow.com/a/18454971/472927
41 changes: 41 additions & 0 deletions src/libcore/sync/atomic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,47 @@ pub fn fence(order: Ordering) {
}


/// A compiler memory barrier.
///
/// `compiler_barrier` does not emit any machine code, but prevents the compiler from re-ordering
/// memory operations across this point. Which reorderings are disallowed is dictated by the given
/// [`Ordering`]. Note that `compiler_barrier` does *not* introduce inter-thread memory
/// synchronization; for that, a [`fence`] is needed.
///
/// The re-ordering prevented by the different ordering semantics are:
///
/// - with [`SeqCst`], no re-ordering of reads and writes across this point is allowed.
/// - with [`Release`], preceding reads and writes cannot be moved past subsequent writes.
/// - with [`Acquire`], subsequent reads and writes cannot be moved ahead of preceding reads.
/// - with [`AcqRel`], both of the above rules are enforced.
///
/// # Panics
///
/// Panics if `order` is [`Relaxed`].
///
/// [`fence`]: fn.fence.html
/// [`Ordering`]: enum.Ordering.html
/// [`Acquire`]: enum.Ordering.html#variant.Acquire
/// [`SeqCst`]: enum.Ordering.html#variant.SeqCst
/// [`Release`]: enum.Ordering.html#variant.Release
/// [`AcqRel`]: enum.Ordering.html#variant.AcqRel
/// [`Relaxed`]: enum.Ordering.html#variant.Relaxed
#[inline]
#[unstable(feature = "compiler_barriers", issue = "41091")]
pub fn compiler_barrier(order: Ordering) {
unsafe {
match order {
Acquire => intrinsics::atomic_singlethreadfence_acq(),
Release => intrinsics::atomic_singlethreadfence_rel(),
AcqRel => intrinsics::atomic_singlethreadfence_acqrel(),
SeqCst => intrinsics::atomic_singlethreadfence(),
Relaxed => panic!("there is no such thing as a relaxed barrier"),
__Nonexhaustive => panic!("invalid memory ordering"),
}
}
}


#[cfg(target_has_atomic = "8")]
#[stable(feature = "atomic_debug", since = "1.3.0")]
impl fmt::Debug for AtomicBool {
Expand Down