Atomic Operations
Having declared atomic variables, we manipulate them using WGSL's built-in atomic functions. These functions bypass standard CPU/GPU caching hazards to interact directly with physical memory, guaranteeing serialized execution.
1. Primary Accessors
To perform basic reads or writes on an atomic variable without modifying it mathematically, you must use the standard load and store built-ins:
atomicLoad(ptr): Safely reads and returns the current value of the atomic variable.atomicStore(ptr, value): Safely writesvalueinto the atomic variable, overwriting the previous value. No value is returned.
2. Read-Modify-Write (RMW) Operations
These functions perform an arithmetic or bitwise operation on the memory location as a single, indivisible hardware transaction:
atomicAdd(ptr, value): Safely addsvalueto the variable.atomicSub(ptr, value): Safely subtractsvaluefrom the variable.atomicMin(ptr, value)/atomicMax(ptr, value): Computes the minimum or maximum betweenvalueand the current variable.atomicAnd(ptr, value)/atomicOr(ptr, value)/atomicXor(ptr, value): Performs a bitwise AND, OR, or XOR on the variable.atomicExchange(ptr, value): Writesvalueto the variable, replacing the current value.
The Power of the "Old Value"
A crucial detail of all RMW functions is that they return the variable's value before the operation occurred.
This old value is highly valuable for multi-threaded coordination. For example, if multiple threads are writing to a shared append-only array, they can use atomicAdd to reserve a unique index (slot) in the array safely:
// Reserving a safe, unique index in a shared array
let write_index = atomicAdd(&shared_queue_counter, 1u);
shared_array[write_index] = thread_computed_result;
Even if hundreds of threads execute this block concurrently, each thread is guaranteed to receive a unique, un-overlapped write_index.
3. Advanced Coordination: Compare-and-Swap
For advanced lock-free algorithms, WGSL provides atomicCompareExchangeWeak, which is the fundamental block for Compare-and-Swap (CAS) routines:
-
atomicCompareExchangeWeak(ptr, compare, value): If the current value atptris equal tocompare, it is overwritten withvalue.It returns a pre-defined structure containing two fields:
old_value: The value of the atomic variable before the operation.exchanged: Aboolindicating whether the exchange occurred (trueifold_value == compare,falseotherwise).
Implementing a CAS Loop
Because WGSL does not natively support operations like atomic multiplication or atomic floating-point additions, you can implement them yourself using a CAS retry loop. The loop continuously reads the old value, computes the desired update, and attempts a compare-exchange until it succeeds:
// Example: Safe atomic multiplication by 2
var<workgroup> my_atomic: atomic<u32>;
fn atomic_multiply_by_two() {
var old = atomicLoad(&my_atomic);
loop {
let new_val = old * 2u;
let res = atomicCompareExchangeWeak(&my_atomic, old, new_val);
// If successful, the exchange occurred and we are done!
if res.exchanged {
break;
}
// If unsuccessful, another thread modified my_atomic first.
// Update old with the actual current value and retry.
old = res.old_value;
}
}
Multi-Variable Memory Barriers
Remember that atomic operations only serialize and coordinate updates for a single memory location. If you need to ensure that non-atomic writes are visible to other threads, or coordinate multi-variable structures, you must combine atomics with execution or memory barriers. Refer to the Barriers & Memory Synchronization section.
Live Operations Demonstration
The simulation on the right displays the results of running sequential atomic operations:
- Initial Value (
initial_val): The counter is initialized safely to \(10\) viaatomicStore(&my_atomic, 10u). - After Addition (
after_add): We add \(5\) safely usingatomicAdd(&my_atomic, 5u). This returns the old value (\(10\)) and sets the new value to \(15\). - After CAS Multiplication (
after_cas_mul): We multiply by \(2\) using our custom Compare-and-Swap retry loop, resulting in a final value of \(30\).
This demonstrates how standard reads, writes, and complex lock-free custom logic (like multiplication) are executed safely and reliably on the GPU.
/*
* Copyright ©2026 Michael R. Bernstein. Licensed under Apache 2.0.
* See root README.md for global project-wide upstream attributions.
*/
struct OperationResults {
initial_val: u32,
after_add: u32,
after_cas_mul: u32,
}
// Statically representing the values after sequential atomic operations
const ops_results = OperationResults(
10u, // initial_val
15u, // after_add (10 + 5)
30u // after_cas_mul (15 * 2)
);
var<workgroup> my_atomic: atomic<u32>;
fn run_operations_demo() {
// Store the initial value safely using a pointer
atomicStore(&my_atomic, 10u);
// Safely add 5 (returns old value 10, new value is 15)
let old_val = atomicAdd(&my_atomic, 5u);
// Safely multiply by 2 using a Compare-and-Swap retry loop
var old = atomicLoad(&my_atomic);
loop {
let new_val = old * 2u;
let res = atomicCompareExchangeWeak(&my_atomic, old, new_val);
// If successful, the exchange occurred and we are done!
if res.exchanged {
break;
}
// If unsuccessful, another thread modified my_atomic first.
// Update old with the actual current value and retry.
old = res.old_value;
}
}