Module mycelium_util::sync::atomic
1.0.0 · source · loom
only.Expand description
Atomic types
Atomic types provide primitive shared-memory communication between threads, and are the building blocks of other concurrent types.
This module defines atomic versions of a select number of primitive
types, including AtomicBool
, AtomicIsize
, AtomicUsize
,
AtomicI8
, AtomicU16
, etc.
Atomic types present operations that, when used correctly, synchronize
updates between threads.
Atomic variables are safe to share between threads (they implement Sync
)
but they do not themselves provide the mechanism for sharing and follow the
threading model of Rust.
The most common way to share an atomic variable is to put it into an Arc
(an
atomically-reference-counted shared pointer).
Atomic types may be stored in static variables, initialized using
the constant initializers like AtomicBool::new
. Atomic statics
are often used for lazy global initialization.
Memory model for atomic accesses
Rust atomics currently follow the same rules as C++20 atomics, specifically atomic_ref
.
Basically, creating a shared reference to one of the Rust atomic types corresponds to creating
an atomic_ref
in C++; the atomic_ref
is destroyed when the lifetime of the shared reference
ends. (A Rust atomic type that is exclusively owned or behind a mutable reference does not
correspond to an “atomic object” in C++, since it can be accessed via non-atomic operations.)
Each method takes an Ordering
which represents the strength of
the memory barrier for that operation. These orderings are the
same as the C++20 atomic orderings. For more information see the nomicon.
Since C++ does not support mixing atomic and non-atomic accesses, or non-synchronized different-sized accesses to the same data, Rust does not support those operations either. Note that both of those restrictions only apply if the accesses are non-synchronized.
use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};
use std::mem::transmute;
use std::thread;
let atomic = AtomicU16::new(0);
thread::scope(|s| {
// This is UB: mixing atomic and non-atomic accesses
s.spawn(|| atomic.store(1, Ordering::Relaxed));
s.spawn(|| unsafe { atomic.as_ptr().write(2) });
});
thread::scope(|s| {
// This is UB: even reads are not allowed to be mixed
s.spawn(|| atomic.load(Ordering::Relaxed));
s.spawn(|| unsafe { atomic.as_ptr().read() });
});
thread::scope(|s| {
// This is fine, `join` synchronizes the code in a way such that atomic
// and non-atomic accesses can't happen "at the same time"
let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed));
handle.join().unwrap();
s.spawn(|| unsafe { atomic.as_ptr().write(2) });
});
thread::scope(|s| {
// This is UB: using different-sized atomic accesses to the same data
s.spawn(|| atomic.store(1, Ordering::Relaxed));
s.spawn(|| unsafe {
let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic);
differently_sized.store(2, Ordering::Relaxed);
});
});
thread::scope(|s| {
// This is fine, `join` synchronizes the code in a way such that
// differently-sized accesses can't happen "at the same time"
let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed));
handle.join().unwrap();
s.spawn(|| unsafe {
let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic);
differently_sized.store(2, Ordering::Relaxed);
});
});
Portability
All atomic types in this module are guaranteed to be lock-free if they’re
available. This means they don’t internally acquire a global mutex. Atomic
types and operations are not guaranteed to be wait-free. This means that
operations like fetch_or
may be implemented with a compare-and-swap loop.
Atomic operations may be implemented at the instruction layer with
larger-size atomics. For example some platforms use 4-byte atomic
instructions to implement AtomicI8
. Note that this emulation should not
have an impact on correctness of code, it’s just something to be aware of.
The atomic types in this module might not be available on all platforms. The atomic types here are all widely available, however, and can generally be relied upon existing. Some notable exceptions are:
- PowerPC and MIPS platforms with 32-bit pointers do not have
AtomicU64
orAtomicI64
types. - ARM platforms like
armv5te
that aren’t for Linux only provideload
andstore
operations, and do not support Compare and Swap (CAS) operations, such asswap
,fetch_add
, etc. Additionally on Linux, these CAS operations are implemented via operating system support, which may come with a performance penalty. - ARM targets with
thumbv6m
only provideload
andstore
operations, and do not support Compare and Swap (CAS) operations, such asswap
,fetch_add
, etc.
Note that future platforms may be added that also do not have support for
some atomic operations. Maximally portable code will want to be careful
about which atomic types are used. AtomicUsize
and AtomicIsize
are
generally the most portable, but even then they’re not available everywhere.
For reference, the std
library requires AtomicBool
s and pointer-sized atomics, although
core
does not.
The #[cfg(target_has_atomic)]
attribute can be used to conditionally
compile based on the target’s supported bit widths. It is a key-value
option set for each supported size, with values “8”, “16”, “32”, “64”,
“128”, and “ptr” for pointer-sized atomics.
Atomic accesses to read-only memory
In general, all atomic accesses on read-only memory are Undefined Behavior. For instance, attempting
to do a compare_exchange
that will definitely fail (making it conceptually a read-only
operation) can still cause a segmentation fault if the underlying memory page is mapped read-only. Since
atomic load
s might be implemented using compare-exchange operations, even a load
can fault
on read-only memory.
For the purpose of this section, “read-only memory” is defined as memory that is read-only in
the underlying target, i.e., the pages are mapped with a read-only flag and any attempt to write
will cause a page fault. In particular, an &u128
reference that points to memory that is
read-write mapped is not considered to point to “read-only memory”. In Rust, almost all memory
is read-write; the only exceptions are memory created by const
items or static
items without
interior mutability, and memory that was specifically marked as read-only by the operating
system via platform-specific APIs.
As an exception from the general rule stated above, “sufficiently small” atomic loads with
Ordering::Relaxed
are implemented in a way that works on read-only memory, and are hence not
Undefined Behavior. The exact size limit for what makes a load “sufficiently small” varies
depending on the target:
target_arch | Size limit |
---|---|
x86 , arm , mips , mips32r6 , powerpc , riscv32 , sparc , hexagon | 4 bytes |
x86_64 , aarch64 , loongarch64 , mips64 , mips64r6 , powerpc64 , riscv64 , sparc64 , s390x | 8 bytes |
Atomics loads that are larger than this limit as well as atomic loads with ordering other
than Relaxed
, as well as all atomic loads on targets not listed in the table, might still be
read-only under certain conditions, but that is not a stable guarantee and should not be relied
upon.
If you need to do an acquire load on read-only memory, you can do a relaxed load followed by an acquire fence instead.
Examples
A simple spinlock:
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{hint, thread};
fn main() {
let spinlock = Arc::new(AtomicUsize::new(1));
let spinlock_clone = Arc::clone(&spinlock);
let thread = thread::spawn(move|| {
spinlock_clone.store(0, Ordering::Release);
});
// Wait for the other thread to release the lock
while spinlock.load(Ordering::Acquire) != 0 {
hint::spin_loop();
}
if let Err(panic) = thread.join() {
println!("Thread had an error: {panic:?}");
}
}
Keep a global count of live threads:
use std::sync::atomic::{AtomicUsize, Ordering};
static GLOBAL_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
// Note that Relaxed ordering doesn't synchronize anything
// except the global thread counter itself.
let old_thread_count = GLOBAL_THREAD_COUNT.fetch_add(1, Ordering::Relaxed);
// Note that this number may not be true at the moment of printing
// because some other thread may have changed static value already.
println!("live threads: {}", old_thread_count + 1);
Structs
- AtomicBool
target_has_atomic_load_store="8"
A boolean type which can be safely shared between threads. - An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- AtomicPtr
target_has_atomic_load_store="ptr"
A raw pointer type which can be safely shared between threads. - An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
- An integer type which can be safely shared between threads.
Enums
- Atomic memory orderings
Constants
- An
AtomicBool
initialized tofalse
. - An
AtomicIsize
initialized to0
. - An
AtomicUsize
initialized to0
.
Functions
- A compiler memory fence.
- An atomic fence.
- spin_loop_hintDeprecatedSignals the processor that it is inside a busy-wait spin-loop (“spin lock”).