Loom is a data-parallelism framework for Zig. It is lightweight and makes it easy to convert sequential loops into parallel pipelines. Its structured APIs and lock-free internals enable safe, efficient multi-core utilization.
- Minimal code changes — Replace
forwithpar_iter(), done - Lock-free internals — Work-stealing deques with ~5-10ns spawn latency
- Structured concurrency — Scoped spawning ensures tasks complete before scope exits
- Composable API — Chain
.map(),.filter(),.reduce()like functional pipelines - Automatic load balancing — Idle threads steal work from busy ones
- Zero external dependencies — Pure Zig, built on beam primitives
// Before:
var sum: i64 = 0;
for (data) |x| sum += x;
// After:
const sum = loom.par_iter(data).reduce(Reducer(i64).sum());Same result. All cores utilized. No manual thread management.
| Function | Description |
|---|---|
par_iter(slice) |
Parallel iterator over mutable slice. Chain with .map(), .filter(), .reduce(), .for_each(). |
par_iter_const(slice) |
Parallel iterator over const slice. Same operations as par_iter. |
par_range(start, end) |
Zero-allocation parallel range iterator. Ideal for index-based parallelism. |
join(fn_a, args_a, fn_b, args_b) |
Fork-join: run two functions in parallel, return both results. |
scope(body_fn) |
Structured concurrency: spawn dynamic tasks, all complete before scope exits. |
ThreadPool.init(alloc, config) |
Create thread pool with configurable thread count and backoff. |
getGlobalPool() |
Get or create the default global thread pool. |
.for_each(fn) |
Apply function to each element in parallel. |
.reduce(Reducer) |
Combine all elements using thread-local accumulation. |
.map(T, fn, alloc) |
Transform elements in parallel, return new slice. |
.filter(pred, alloc) |
Select matching elements in parallel. |
.find(pred) |
Find first element matching predicate. |
.count(pred) |
Count elements matching predicate. |
.any(pred) / .all(pred) |
Check if any/all elements match predicate. |
.withContext(&ctx) |
Pass shared context to parallel operations. |
.withMinChunk(n) |
Set minimum chunk size before parallelizing. |
Reducer(T).sum() |
Built-in reducer for summation. |
Reducer(T).product() |
Built-in reducer for multiplication. |
Reducer(T).min() / .max() |
Built-in reducers for min/max. |
For detailed examples, see FAQ and samples.
Loom provides Rayon-like data parallelism with a layered architecture: primitives (Deque, DequeChannel), thread pool infrastructure, fork-join primitives, and parallel iterators.
+---------------------------------------------------------------------+
| Loom Architecture |
+---------------------------------------------------------------------+
+---------------------------------------+
| Global MPMC Queue |
| (overflow + external thread access) |
+------------------+--------------------+
|
+------------------------------+------------------------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| Worker 0 | | Worker 1 | | Worker N |
| +-----------+ | | +-----------+ | | +-----------+ |
| |Local Deque| | <--steal--> | |Local Deque| | <--steal--> | |Local Deque| |
| |(push/pop) | | | |(push/pop) | | | |(push/pop) | |
| +-----------+ | | +-----------+ | | +-----------+ |
+---------------+ +---------------+ +---------------+
Owner operations: Thief operations:
+----------------+ +------------------+
| spawn() ~5-10ns| | steal() ~20-50ns |
| pop() ~3-10ns| +------------------+
+----------------+
Layer 4: par_iter() -> Parallel iterators (map, reduce, filter, for_each)
Layer 3: join/scope -> Fork-join primitives (structured concurrency)
Layer 2: ThreadPool -> Work-stealing thread pool
Layer 1: Deque/Channel -> Lock-free primitives (from beam libs)
const loom = @import("loom");
// Binary fork-join (divide-and-conquer)
const left, const right = loom.join(
computeLeft, .{data[0..mid]},
computeRight, .{data[mid..]},
);
// Scoped spawning (structured concurrency)
loom.scope(struct {
fn body(s: *loom.Scope) void {
for (items) |item| {
s.spawn(process, .{item});
}
}
}.body);
// Parallel iteration
loom.par_iter(items).for_each(process);
const sum = loom.par_iter(items).reduce(loom.Reducer(i64).sum());ThreadPool:
ThreadPool.init(allocator, config)- Create pool with configurable threads/backoffpool.spawn(task)- Submit task (~5-10ns from worker, ~20-50ns external)pool.deinit()- Shutdown and free resourcesgetGlobalPool()- Get/create default global pool
Fork-Join:
join(fn_a, args_a, fn_b, args_b)- Binary fork-joinjoinOnPool(pool, ...)- Fork-join on specific poolscope(body_fn)- Structured concurrency containerscopeOnPool(pool, body_fn)- Scope on specific pool
Parallel Iterators:
par_iter(slice)- Parallel iterator over mutable slicepar_iter_const(slice)- Parallel iterator over const slicepar_range(start, end)- Zero-allocation range iterator
| Operation | Latency | Description |
|---|---|---|
| Task spawn (worker) | ~5-10ns | Local deque push, no CAS |
| Task spawn (external) | ~20-50ns | Global queue enqueue |
| Task receive | ~10-50ns | Local pop -> global -> steal |
| join() | ~overhead + work | Spawn right, run left locally |
| scope.spawn() | ~50-100ns | Arena alloc + pool spawn |
| par_iter().for_each() | ~overhead + work | Chunk + parallel spawn |
- Work-stealing pattern - Busy threads steal from idle threads
- Steal-while-wait - Waiting threads help execute other tasks
- Type-erased tasks - 24 bytes (fn ptr + context + scope)
- Sharded arenas - One arena per worker eliminates contention
- Adaptive splitting - 2x oversubscription with configurable min chunk
- Cache locality - LIFO for owner, FIFO for thieves
// Pattern 1: Parallel sum with reduction
const sum = loom.par_iter(numbers)
.reduce(loom.Reducer(i64).sum());
// Pattern 2: Parallel transformation
const doubled = try loom.par_iter(numbers)
.map(i64, struct {
fn double(x: i64) i64 { return x * 2; }
}.double, allocator);
defer allocator.free(doubled);
// Pattern 3: Divide-and-conquer (parallel quicksort)
fn parallelSort(data: []i32) void {
if (data.len < 1000) {
std.sort.pdq(i32, data, {}, std.sort.asc(i32));
return;
}
const pivot = partition(data);
loom.join(
parallelSort, .{data[0..pivot]},
parallelSort, .{data[pivot..]},
);
}
// Pattern 4: Context-based parallel processing
const Context = struct {
threshold: f64,
counter: *std.atomic.Value(usize),
};
var count = std.atomic.Value(usize).init(0);
const ctx = Context{ .threshold = 100.0, .counter = &count };
loom.par_iter(data)
.withContext(&ctx)
.forEach(struct {
fn check(c: *const Context, value: *f64) void {
if (value.* > c.threshold) {
_ = c.counter.fetchAdd(1, .monotonic);
}
}
}.check);
// Pattern 5: Parallel file processing with par_range
loom.par_range(0, num_chunks)
.withContext(&read_ctx)
.forEach(struct {
fn readChunk(ctx: *const ReadContext, chunk_idx: usize) void {
const start = chunk_idx * ctx.chunk_size;
_ = ctx.file_handle.pread(ctx.buffer[start..], start) catch {};
}
}.readChunk);par_iter(slice)
.withMinChunk(1000) // Sequential threshold
.withMaxChunks(8) // Cap parallelism
.withPool(custom_pool) // Use specific pool
.withContext(&ctx) // Pass shared context
.withAlloc(allocator) // For filter/map
// Terminal operations:
.for_each(fn) // Apply to each element
.reduce(Reducer) // Combine all elements
.map(T, fn, allocator) // Transform elements
.filter(pred, allocator) // Select elements
.find(pred) // First match
.all(pred) // All match?
.any(pred) // Any match?
.count(pred) // Count matches| Method | Signature | Description |
|---|---|---|
forEach |
fn(Ctx, *T) void |
Apply to each element |
forEachIndexed |
fn(Ctx, usize, *T) void |
Apply with index |
count |
fn(Ctx, T) bool |
Count matching elements |
any |
fn(Ctx, T) bool |
Check if any match |
all |
fn(Ctx, T) bool |
Check if all match |
find |
fn(Ctx, T) bool |
Find first match |
map |
fn(Ctx, T) U |
Transform elements |
mapIndexed |
fn(Ctx, usize, T) U |
Transform with index |
filter |
fn(Ctx, T) bool |
Select matching |
chunks |
fn(Ctx, usize, []T) void |
Process mutable chunks |
chunksConst |
fn(Ctx, usize, []const T) void |
Process const chunks |
Reducer(i64).sum() // Identity: 0, combine: +
Reducer(i64).product() // Identity: 1, combine: *
Reducer(i32).min() // Identity: maxInt, combine: @min
Reducer(i32).max() // Identity: minInt, combine: @max
BoolReducer.all() // Identity: true, combine: and
BoolReducer.any() // Identity: false, combine: orconst pool = try ThreadPool.init(allocator, .{
.num_threads = 8, // null = CPU count
.backoff_mode = .balanced, // .low_latency, .balanced, .power_saving
});
defer pool.deinit();| Mode | Use Case | Spin | Sleep |
|---|---|---|---|
low_latency |
Real-time, gaming | 64 | 100us |
balanced |
General purpose | 32 | 1ms |
power_saving |
Background tasks | 8 | 10ms |
Splitter.default() // adaptive(1024)
Splitter.fixed(100) // Fixed chunk size
Splitter.adaptive(1000) // Adaptive with min 1000
Splitter.recursive(256) // For divide-and-conquerContext is shared across all worker threads:
- Read-only fields: No synchronization needed
- Counters: Use
std.atomic.Value(T)with.monotonic - Complex mutations: Use mutexes
- Best performance: Thread-local accumulation with final merge
# Run Loom tests
zig build test-loom
# Run Loom benchmarks
zig build bench-loom -Doptimize=ReleaseFast
# Run Loom samples (API showcase)
zig build samples-loom- Zig version: 0.13.0 or later
- Platform: Any platform supported by Zig's standard library
- Dependencies: Part of zigbeam
- Primitives: Uses beam-deque and beam-deque-channel
- FAQ.md - Common questions and answers
- LOOM_IMPLEMENTATION.md - Architecture and design
- loom_api_showcase.zig - Comprehensive API examples
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.