Zigroutines: new concurrency lib for Zig

zigroutines


zigroutines is a library that brings stackful M:N concurrency to Zig, inspired by Go’s task model, channels, and select, but with a philosophy that fits Zig perfectly: nothing global, nothing hidden, pay only for what you enable.

You construct a Runtime with your chosen scheduler (FIFO, work‑stealing, priority, or even 1:1 OS threads), I/O backend (poll, IOCP, io_uring, or none), and optional metrics. Every task gets a fixed 2 KiB stack (no silent growth, no per‑task configuration) - deep recursion and large buffers go on the heap. The library provides:

  • CSP: Channel(T) with rendezvous/buffered, multi‑arm select (up to 32 arms each for send/recv), backpressure policies (drop_newest, drop_oldest, …).
  • Structured concurrency: Scope/Nursery with deadlines and cooperative cancellation.
  • Synchronisation primitives that park the task, not the OS thread: Mutex, RwLock, Semaphore, RateLimiter, Notify, Watch(T).
  • Pluggable I/O - integrate poll, IOCP, or io_uring only when you need it.
  • Observability - optional atomic metrics and tracing hooks.

Design decisions:

The most interesting challenge was deciding what not to hide. I deliberately avoided a global runtime - every Runtime is explicit, so you can embed it into your own event loop or run multiple independent runtimes. This also makes it easy to test and reason about resource usage.

Another key decision: fixed‑size stacks (2 KiB). This is a trade‑off - you lose the flexibility of growing stacks, but you gain predictability and avoid GC‑like copying. To compensate, the library provides a spawnLeaf for stackless, run‑to‑completion work, and the stack pool + guard page/canary give you control over overflow and memory usage.

I also made I/O a plugin - the core scheduler doesn’t assume a net‑poller. This keeps the runtime tiny for CPU‑bound tasks, and you enable I/O only when needed.

Finally, the cancellation model is cooperative - you poll a CancelToken or use select with a cancel arm. This avoids the complexity and unpredictability of preemptive cancellation, and fits well with the explicit nature of the library.

AI / LLM usage disclosure

I used an LLM (Grok) only to the README.md file and to generate code‑review summarie

Supported Zig versions

0.16 and 0.17‑dev

3 Likes

It looks amazing would you like to add it to:

Curious, how do you use Zig with fixed 2 KB stacks? The standard library routinely allocates chunks like 64+ KB on stack.

Anyway, interesting to see more competion in the space, I’ll compare the performance to zio.

1 Like

In the standard library, as far as I know, 64kb is the default because std.Threads, etc. lives in the real core/processor stack, for example: deep libc/runtime calls, signal handlers, TLS and safety margin

And I have my own 2kb buffer, fixed and without growth. As well as its own context switch, in which only registers and stack pointer are saved and no OS thread is created. Only your task code + a thin trampoline is spinning on this stack, not the entire OS runtime. I have everything heavy on the pile. There is a spawnLeaf without its own stack (run to completion). As well as guard page/canary/corruption mechanisms. This is a deliberate trade-off

What is the example use case? I can’t find any situation where 2KB would be enough in a real world Zig program, unless you never use the stdlib. And be methodical with only ever allocating on heap.

In any case, I can’t build your project. Build system doesn’t work on Zig 0.16 and if I switch to 0.17, it tries to use std.posix.nanosleep which was removed in 0.16.

I also find the claims about zio in your readme questionable, especially “partial” about channels, but that’s okay.

1 Like

That’s enough wherever you write concurrent control flow. This is the same class of tasks for which people take goroutines.: thousands of waiters, pipeline, actors, I/O sessions. If you need a heavy computing branch with huge local arrays, it should not live like a mountain with a naive stack: heap, leaf, or a separate worker. This is a control plane/data plane separation. OS-thread (and std.Thread) have a large stack (tens/hundreds of KiB or more), because the OS thread is a full-fledged unit: libc, deep call chain, stock for everything.
We have a user buffer on which only the task code is spinning. Switching is not via the core, but via its context switch. Therefore, comparing 2 KiB fiber vs 64 KiB std thread is like comparing a bicycle with a truck: different tasks. But in general, I plan to add the option of choosing the size in the next updates if it is really justified and necessary.

Can you put a disclosure about the AI usage in your project? It seems to be missing from the original post

3 Likes

Used Grok Ai for README.md I don’t know how to write beautiful and understandable explanations of projects (although I never received a beautiful md file XD). And I used Grok to review my code in architectural solutions in order to receive text reports on potential problems and holes

the ask is to put this in the main post, and potentially use the llm tag.

2 Likes