Barriers & Memory Synchronization
In a GPGPU compute shader, thousands of threads execute concurrently. When these threads cooperate using shared memory (like var<workgroup>), we need a way to synchronize them to avoid data races and corruption.
WGSL provides barriers to synchronize threads within the same workgroup.
Execution vs. Memory Synchronization
A barrier performs two critical tasks:
- Execution Synchronization (Control Barrier): It blocks execution of any thread in the workgroup until every thread in that workgroup has reached the barrier. This ensures all threads are aligned in time before proceeding.
- Memory Synchronization (Memory Barrier): It ensures that all memory reads/writes performed by any thread before hitting the barrier are completed and made fully visible to all other threads in the workgroup.
WGSL Synchronization Built-ins
There are two primary barrier functions in WGSL:
workgroupBarrier()
This function synchronizes all threads within the local workgroup. It forces all threads to wait, and flushes all var<workgroup> memory operations to ensure visibility.
workgroup_shared_array[local_idx] = input_array[global_idx];
workgroupBarrier(); // Sync: wait for all threads to write to the workgroup array
let left_neighbor = workgroup_shared_array[(local_idx + 1u) % 64u];
storageBarrier()
This function is similar to workgroupBarrier(), but it specifically flushes reads and writes to storage buffers (var<storage, read_write>), ensuring visibility of storage memory accesses across threads in the workgroup.
Important
Barrier Constraints: Barriers can only be called in compute shaders and only within uniform control flow (meaning all threads in the workgroup must execute the barrier. Calling a barrier inside an if statement where some threads execute it and others do not leads to undefined behavior or deadlocks!).
Atomics vs. Barriers
While memory barriers synchronize execution and memory visibility across multiple variables inside a workgroup, you can also perform safe, indivisible operations on individual 32-bit integers using Atomics. Refer to the Atomics section.
Simulating Synchronization
In the shader code on the right, we show how thread coordination and safety flags are tracked. The visualizer outputs the status.
/*
* Copyright ©2026 Michael R. Bernstein. Licensed under Apache 2.0.
* See root README.md for global project-wide upstream attributions.
*/
struct SyncStatus {
has_raced: u32,
barrier_passed: u32,
}
// 1. Declare workgroup memory (SRAM) shared across all threads in the workgroup.
// Note: Workgroup variables must be declared at the module scope (top-level).
var<workgroup> temp_cache: array<f32, 64>;
// Simulating a synchronized collaborative reduction/shift.
// Under safe synchronization, has_raced is 0u, and barrier_passed is 1u.
const sync_status = SyncStatus(
0u, // has_raced (safe execution guaranteed by barrier)
1u // barrier_passed (all threads reached and aligned successfully)
);
// Illustrative function showing the collaborative workgroup load pattern.
fn run_safe_collaborative_load(local_idx: u32) -> f32 {
// 2. Each thread loads its matching item from the slow global memory into local SRAM.
// temp_cache[local_idx] = global_input[local_idx];
// 3. We MUST wait for all threads to finish their load operations before we read.
// If we do not put workgroupBarrier() here, a thread might read adjacent elements
// before the thread responsible for writing them has executed, creating a DATA RACE!
workgroupBarrier();
// 4. Safe readback: It is now guaranteed that every cache slot is fully written.
let neighbor = temp_cache[(local_idx + 1u) % 64u];
return neighbor * 2.0;
}