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
| Operation | Success Ordering | Failure Ordering | Purpose |
|---|---|---|---|
| compare_exchange | Acquire | Relaxed | Synchronize on successful registration |
| swap | Acquire | N/A | Synchronize when removing handler |
| load | Acquire | N/A | Synchronize 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.