No description
  • Zig 92.5%
  • Shell 7.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 4d2a7d2097
All checks were successful
test / test (push) Successful in 24s
Merge branch 'dev'
2026-10-06 11:27:03 +02:00
.forgejo/workflows Migrate to Zig 0.17 2026-10-06 11:25:58 +02:00
scripts Migrate to Zig 0.17 2026-10-06 11:25:58 +02:00
src sync: timedWait and a monotonic clock 2026-09-13 11:37:45 +02:00
.devkit.conf Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
.gitignore scripts: one setup-tools, and the clone moves to .tools/ 2026-09-22 21:05:44 +02:00
build.zig Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
build.zig.zon Migrate to Zig 0.17 2026-10-06 11:25:58 +02:00
DESIGN.md scripts: one setup-tools, and the clone moves to .tools/ 2026-09-22 21:05:44 +02:00
LICENSE Extract the worker-pool → consumer thread handoff from comb 2026-08-08 23:35:47 +02:00
README.md Migrate to Zig 0.17 2026-10-06 11:25:58 +02:00

handoff

Worker threads produce results; one consumer thread reads them. This is the bit in between. Zig 0.17, 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.