No description
  • Zig 96.2%
  • Shell 3.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 59bdaa114b docs: both consumers have migrated their sync
sift landed it today, so no copy survives outside this package -- which
was the point. Snapshot(T) is still comb-only, pending publish-by-swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:16:00 +02:00
scripts Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
src Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
.devkit.conf Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
.gitignore Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
build.zig Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
build.zig.zon Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
DESIGN.md docs: both consumers have migrated their sync 2026-08-09 00:16:00 +02:00
LICENSE Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
README.md docs: trim the README 2026-08-08 23:46:25 +02:00

handoff

Worker threads produce results; one consumer thread reads them. This is the bit in between. Zig 0.16, no dependencies.

Extracted from comb, shared with sift. Design notes in DESIGN.md.

sync

Zig 0.16 moved std.Thread.Mutex and Condition behind the std.Io vtable, so taking a lock means threading an Io through every call site. For a library that spawns its own OS threads that is pure friction. sync binds libc pthread directly.

  • Mutex: lock, unlock, deinit
  • Condition: wait(mutex), signal, broadcast, deinit

Snapshot(T)

A published result set with a version counter. Producers append under a lock and bump the generation; the consumer calls tick(), which refreshes a private copy only if the generation moved, then reads it unlocked via items().

So you can poll as often as you like, copy only when something actually changed, and never iterate results while holding the producers' lock.

Producer side: append, appendSlice, replace, clear, setRunning. Consumer side: tick, items, isRunning, count.

Beyond that:

  • If T has clone and deinit, the snapshot owns its items: they are cloned in, each buffer into its own arena, so a round frees in one reset and the two buffers can never dangle each other. Without those decls T is copied by value and the arena code compiles away.
  • beginRound() empties the published set without bumping the generation, so the consumer holds the previous view instead of flashing empty for a frame between rounds. The next publish exposes the change.
  • tailInto(from, out) drains incrementally, for consumers that stream results out rather than re-clone the whole view every frame.

Not included: the worker pool, and job scheduling or coalescing. Those differ per engine.

Use

const handoff = @import("handoff");

var snap: handoff.Snapshot(Result) = .{};
defer snap.deinit(allocator);

// worker threads
try snap.append(allocator, result);

// consumer thread
const status = try snap.tick(allocator);
if (status.changed) render(snap.items());

zig build test runs the suite. Needs libc (pthread); the module sets link_libc itself.

MIT.