Atomic Operations

Relevant source files

This document explains the atomic operations that form the foundation of the HandlerTable's lock-free design. It covers the specific atomic primitives used, memory ordering guarantees, and how these operations ensure thread safety without traditional locking mechanisms.

For information about the overall implementation architecture and memory safety aspects, see Memory Layout and Safety. For practical usage of the lock-free API, see API Reference.

Atomic Data Structure Foundation

The HandlerTable uses an array of AtomicUsize values as its core storage mechanism. Each slot in the array can atomically hold either a null value (0) or a function pointer cast to usize.

Core Atomic Array Structure

flowchart TD
subgraph States["Slot States"]
    Empty["Empty: 0"]
    Occupied["Occupied: handler as usize"]
end
subgraph AtomicSlots["Atomic Storage Slots"]
    Slot0["AtomicUsize[0]Value: 0 or fn_ptr"]
    Slot1["AtomicUsize[1]Value: 0 or fn_ptr"]
    SlotN["AtomicUsize[N-1]Value: 0 or fn_ptr"]
end
subgraph HandlerTable["HandlerTable<N>"]
    Array["handlers: [AtomicUsize; N]"]
end

Array --> Slot0
Array --> Slot1
Array --> SlotN
Slot0 --> Empty
Slot0 --> Occupied
Slot1 --> Empty
Slot1 --> Occupied
SlotN --> Empty
SlotN --> Occupied

Sources: src/lib.rs(L14 - L16)  src/lib.rs(L21 - L23) 

The atomic array is initialized with zero values using const generics, ensuring compile-time allocation without heap usage. Each AtomicUsize slot represents either an empty handler position (value 0) or contains a function pointer cast to usize.

Core Atomic Operations

The HandlerTable implements three primary atomic operations that provide lock-free access to the handler storage.

Compare-Exchange Operation

The compare_exchange operation is used in register_handler to atomically install a new handler only if the slot is currently empty.

sequenceDiagram
    participant ClientThread as "Client Thread"
    participant AtomicUsizeidx as "AtomicUsize[idx]"
    participant MemorySystem as "Memory System"

    ClientThread ->> AtomicUsizeidx: "compare_exchange(0, handler_ptr, Acquire, Relaxed)"
    AtomicUsizeidx ->> MemorySystem: "Check current value == 0"
    alt Current value is 0
        MemorySystem ->> AtomicUsizeidx: "Replace with handler_ptr"
        AtomicUsizeidx ->> ClientThread: "Ok(0)"
        Note over ClientThread: "Registration successful"
    else Current value != 0
        MemorySystem ->> AtomicUsizeidx: "Keep current value"
        AtomicUsizeidx ->> ClientThread: "Err(current_value)"
        Note over ClientThread: "Registration failed - slot occupied"
    end

Sources: src/lib.rs(L34 - L36) 

The operation uses Ordering::Acquire for success and Ordering::Relaxed for failure, ensuring proper synchronization when a handler is successfully installed.

Atomic Swap Operation

The swap operation in unregister_handler atomically replaces the current handler with zero and returns the previous value.

flowchart TD
Start["swap(0, Ordering::Acquire)"]
Read["Atomically read current value"]
Write["Atomically write 0"]
Check["Current value != 0?"]
Convert["transmute<usize, fn()>(value)"]
ReturnSome["Return Some(handler)"]
ReturnNone["Return None"]

Check --> Convert
Check --> ReturnNone
Convert --> ReturnSome
Read --> Write
Start --> Read
Write --> Check

Sources: src/lib.rs(L46)  src/lib.rs(L47 - L51) 

Atomic Load Operation

The load operation in handle reads the current handler value without modification, using acquire ordering to ensure proper synchronization.

flowchart TD
subgraph Results["Possible Results"]
    Success["Return true"]
    NoHandler["Return false"]
end
subgraph LoadOperation["Atomic Load Process"]
    Load["load(Ordering::Acquire)"]
    Check["Check value != 0"]
    Transmute["unsafe transmute<usize, Handler>"]
    Call["handler()"]
end

Call --> Success
Check --> NoHandler
Check --> Transmute
Load --> Check
Transmute --> Call

Sources: src/lib.rs(L62)  src/lib.rs(L63 - L66) 

Memory Ordering Guarantees

The atomic operations use specific memory ordering to ensure correct synchronization across threads while minimizing performance overhead.

Ordering Usage Patterns

OperationSuccess OrderingFailure OrderingPurpose
compare_exchangeAcquireRelaxedSynchronize on successful registration
swapAcquireN/ASynchronize when removing handler
loadAcquireN/ASynchronize when reading handler

Sources: src/lib.rs(L35)  src/lib.rs(L46)  src/lib.rs(L62) 

Acquire Ordering Semantics

flowchart TD
subgraph Ordering["Memory Ordering Guarantee"]
    Sync["Acquire ensures visibilityof prior writes"]
end
subgraph Thread2["Thread 2 - Handle Event"]
    T2_Load["load(Acquire)"]
    T2_Read["Read handler data"]
    T2_Call["Call handler"]
end
subgraph Thread1["Thread 1 - Register Handler"]
    T1_Write["Write handler data"]
    T1_CAS["compare_exchange(..., Acquire, ...)"]
end

T1_CAS --> Sync
T1_CAS --> T2_Load
T1_Write --> T1_CAS
T2_Load --> Sync
T2_Load --> T2_Read
T2_Read --> T2_Call

Sources: src/lib.rs(L35)  src/lib.rs(L62) 

The Acquire ordering ensures that when a thread successfully reads a non-zero handler value, it observes all memory writes that happened-before the handler was stored.

Lock-Free Properties

The atomic operations provide several lock-free guarantees essential for kernel-level event handling.

Wait-Free Registration

stateDiagram-v2
[*] --> Attempting : "register_handler(idx, fn)"
Attempting --> Success : "CAS succeeds (slot was 0)"
Attempting --> Failure : "CAS fails (slot occupied)"
Success --> [*] : "Return true"
Failure --> [*] : "Return false"
note left of Success : ['"Single atomic operation"']
note left of Failure : ['"No retry loops"']

Sources: src/lib.rs(L30 - L37) 

The registration operation is wait-free - it completes in a bounded number of steps regardless of other thread activity. Either the compare-exchange succeeds immediately, or it fails immediately if another handler is already registered.

Non-Blocking Event Handling

The handle operation never blocks and provides consistent performance characteristics:

  • Constant Time: Single atomic load operation
  • No Contention: Multiple threads can simultaneously handle different events
  • Real-Time Safe: No unbounded waiting or priority inversion

Sources: src/lib.rs(L58 - L70) 

Function Pointer Storage Mechanism

The atomic operations work on usize values that represent function pointers, requiring careful handling to maintain type safety.

Pointer-to-Integer Conversion

flowchart TD
subgraph Output["Output Types"]
    Recovered["Handler (fn())"]
end
subgraph Retrieval["Type Recovery"]
    Load["AtomicUsize::load"]
    Transmute["unsafe transmute<usize, fn()>"]
end
subgraph Storage["Atomic Storage"]
    Atomic["AtomicUsize value"]
end
subgraph Conversion["Type Conversion"]
    Cast["handler as usize"]
    Store["AtomicUsize::store"]
end
subgraph Input["Input Types"]
    Handler["Handler (fn())"]
end

Atomic --> Load
Cast --> Store
Handler --> Cast
Load --> Transmute
Store --> Atomic
Transmute --> Recovered

Sources: src/lib.rs(L35)  src/lib.rs(L48)  src/lib.rs(L64) 

The conversion relies on the platform guarantee that function pointers can be safely cast to usize and back. The unsafe transmute operations are necessary because the atomic types only work with integer values, but the conversions preserve the original function pointer values exactly.