Browse the docs

silk.concurrency.atomic.

Atomic memory operations on raw pointers, with explicit memory orderings.

When to use#

Use these operations to build synchronization: counters, flags, lock words, and queues that several threads read and write at the same time. For most code, the types in silk.concurrency.sync (Arc, Mutex, Parker) are easier and safer.

Details#

Each operation is one indivisible access to a naturally aligned integer or data pointer. The MemoryOrder argument is static, so the operation and its ordering are fixed at compile time. The model is the C++20 model: an acquire load that reads a release store makes every write before that store visible after the load.

Atomic.wait, Atomic.notifyOne and Atomic.notifyAll are a futex: a thread sleeps until the value at an address changes. They work on Darwin on aarch64, Linux with GNU libc on x86_64 or aarch64, and wasm32.

Gotchas#

  • A plain access that races with any other access to the same location is undefined behavior, also when the other access is atomic. Make every access to a shared location atomic, with the same width.

  • All operations except Atomic.fence are unsafe. The caller proves that the pointer is live and aligned.

Examples#

Count and claim a flag#

silk
import silk.concurrency.atomic { Atomic, CompareExchange, MemoryOrder }
import silk.mem.pointer { Pointer }

// Claims a one-time flag. Only the first caller gets true.
fn claim(flag: *mut u32) -> bool {
  return match unsafe Atomic.compareExchange<u32>(
    flag,
    0,
    1,
    MemoryOrder.Acquire,
    MemoryOrder.Relaxed
  ) {
    CompareExchange<u32>.Exchanged => true
    CompareExchange<u32>.Observed { value } => false
  }
}

pub fn main() -> i32 {
  let mut hits: u64 = 0
  let counter = Pointer.fromMutRef(&mut hits)

  // fetchAdd returns the value before the add.
  let before = unsafe Atomic.fetchAdd<u64>(counter, 5, MemoryOrder.Relaxed)
  if before != 0 || unsafe Atomic.load<u64>(counter, MemoryOrder.Relaxed) != 5 {
    return 1
  }

  let mut claimed: u32 = 0
  let flag = Pointer.fromMutRef(&mut claimed)
  if !claim(flag) || claim(flag) {
    return 1
  }
  return 0
}

Import as Atomic with import silk.concurrency.atomic { Atomic }.

Public declarations: 3.

MemoryOrder#

silk
pub enum MemoryOrder

The ordering constraint of one atomic operation.

Details#

The model is the C++20 model: an acquire load that reads a release store makes every write before that store visible after the load. The order is a static argument, so the operation and its ordering are fixed at compile time.

Relaxed#

silk
Relaxed

Atomicity only. It orders no other access.

Acquire#

silk
Acquire

A load that later reads and writes cannot move before.

Release#

silk
Release

A store that earlier reads and writes cannot move after.

AcquireRelease#

silk
AcquireRelease

Both acquire and release. Use it for read-modify-write operations and fences.

SequentiallyConsistent#

silk
SequentiallyConsistent

Acquire and release, plus one total order over every sequentially consistent operation.

CompareExchange#

silk
pub union CompareExchange<T>

The outcome of a compare-exchange.

Exchanged#

silk
Exchanged

The pointee equaled the expected value and now holds the desired one.

Observed#

silk
Observed

The pointee held value, and nothing was stored.

Field value#

silk
pub value: T

The value read instead of the expected one.

Atomic#

silk
pub struct Atomic

The atomic operations over raw pointers, with explicit memory orderings.

When to use#

Use these operations to build synchronization: counters, flags, lock words, and queues that several threads read and write at the same time. For most code, the types in silk.concurrency.sync (Arc, Mutex, Parker) are easier and safer.

Details#

Each operation is one indivisible access to a naturally aligned integer or data pointer. T is one of u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, or a data pointer for load, store, swap, and the compare-exchanges. The arithmetic and bitwise operations take integers only and wrap on overflow.

Gotchas#

  • A plain access that races with any other access to the same location is undefined behavior, also when the other access is atomic. Make every access to a shared location atomic, with the same width.

  • All operations except Atomic.fence are unsafe. The caller proves that the pointer is live and aligned.

Implementation Atomic#

silk
impl Atomic

Associated function load#

silk
pub unsafe fn load<T: Copy>(pointer: *const T, static order: MemoryOrder) -> T

Reads the pointee atomically.

Safety#

The pointer must address live, initialized, naturally aligned storage for T. The order must be Relaxed, Acquire, or SequentiallyConsistent. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function store#

silk
pub unsafe fn store<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> ()

Writes value to the pointee atomically.

Safety#

The pointer must address live, naturally aligned, writable storage for T. The order must be Relaxed, Release, or SequentiallyConsistent. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function swap#

silk
pub unsafe fn swap<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Replaces the pointee with value and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function compareExchange#

silk
pub unsafe fn compareExchange<T: Copy>(pointer: *mut T, expected: T, desired: T, static success: MemoryOrder,
    static failure: MemoryOrder) -> CompareExchange<T>

Stores desired if the pointee equals expected. Otherwise it returns the value it read.

Details#

success orders the exchange. failure orders the read when nothing is stored. failure must be Relaxed, Acquire, or SequentiallyConsistent.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function compareExchangeWeak#

silk
pub unsafe fn compareExchangeWeak<T: Copy>(pointer: *mut T, expected: T, desired: T, static success: MemoryOrder,
    static failure: MemoryOrder) -> CompareExchange<T>

Stores desired if the pointee equals expected. Otherwise it returns the value it read. It can also fail spuriously: return Observed with a value equal to expected.

When to use#

Use it in a retry loop. Use Atomic.compareExchange when one attempt must decide.

Details#

This spurious failure lets a load-linked, store-conditional target use one attempt per call. Call it in a loop that retries on any observed value. success orders the exchange. failure orders the read when nothing is stored. failure must be Relaxed, Acquire, or SequentiallyConsistent.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchAdd#

silk
pub unsafe fn fetchAdd<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Adds value to the pointee, wrapping on overflow, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchSubtract#

silk
pub unsafe fn fetchSubtract<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Subtracts value from the pointee, wrapping on overflow, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchBitAnd#

silk
pub unsafe fn fetchBitAnd<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Stores the bitwise and of the pointee and value, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchBitOr#

silk
pub unsafe fn fetchBitOr<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Stores the bitwise or of the pointee and value, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchBitXor#

silk
pub unsafe fn fetchBitXor<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Stores the bitwise exclusive or of the pointee and value, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchMinimum#

silk
pub unsafe fn fetchMinimum<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Stores the smaller of the pointee and value, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function fetchMaximum#

silk
pub unsafe fn fetchMaximum<T: Copy>(pointer: *mut T, value: T, static order: MemoryOrder) -> T

Stores the larger of the pointee and value, and returns the previous value.

Safety#

The pointer must address live, initialized, naturally aligned, writable storage for T. Every access to the same location that can happen at the same time must be atomic and have the same width.

Associated function wait#

silk
pub unsafe fn wait(pointer: *const u32, expected: u32, timeout: Option<u64>) -> bool

Sleeps while the pointee equals expected, until a notify, the timeout, or a spurious wakeup.

Details#

The comparison and the sleep are one atomic step against Atomic.notifyOne and Atomic.notifyAll. Thus a notify after a store that changed the pointee is never lost.

timeout is a relative bound in nanoseconds. Option.None waits with no bound. The result is false only when the timeout ended the wait. A true result tells nothing about the pointee, so check the condition again in a loop.

It works on Darwin on aarch64, Linux with GNU libc on x86_64 or aarch64, and wasm32.

Gotchas#

Single-threaded WebAssembly has no other thread to change the pointee. A wait that sleeps with no timeout traps, and a wait with a timeout returns false at once.

Safety#

The pointer must address live, naturally aligned storage for a u32. Every other access to it must be atomic.

Associated function notifyOne#

silk
pub unsafe fn notifyOne(pointer: *const u32) -> ()

Wakes at least one thread blocked in Atomic.wait on pointer, if any is.

Details#

Store the new value before the notify. A waiter that has not yet slept sees the changed value and does not sleep, so the wakeup is never lost. It works on Darwin on aarch64, Linux with GNU libc on x86_64 or aarch64, and wasm32.

Safety#

The pointer must address live, naturally aligned storage for a u32.

Associated function notifyAll#

silk
pub unsafe fn notifyAll(pointer: *const u32) -> ()

Wakes every thread blocked in Atomic.wait on pointer.

Details#

Store the new value before the notify. A waiter that has not yet slept sees the changed value and does not sleep, so the wakeup is never lost. It works on Darwin on aarch64, Linux with GNU libc on x86_64 or aarch64, and wasm32.

Safety#

The pointer must address live, naturally aligned storage for a u32.

Associated function fence#

silk
pub fn fence(static order: MemoryOrder) -> ()

Orders memory accesses around this point without accessing memory itself.

Details#

The order must not be Relaxed. A release fence before a relaxed store, paired with an acquire fence after a relaxed load that reads it, synchronizes like a release store and an acquire load.

Category: Concurrency.

Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2.

View the source of this page