Concurrency in Serene's Runtime

lxsameer1 pts0 comments

Concurrency in Serene's Runtime - Part 1: Choosing the Building Blocks

Concurrency in Serene's Runtime - Part 1: Choosing the Building Blocks

#serene

#languages

#C

#concurrency

#runtime

#series

- 2026-07-30

This post is the first part of a three part series on concurrency in Serene’s runtime.

Choosing the building blocks (this post)

Fibers, the scheduler and the reactor

A tiny HTTP server

As you may know, I’m working on a programming language called Serene. It has a<br>runtime library that provides the crucial bits every program needs while it’s running.<br>The runtime library is perfectly usable outside Serene too; in fact, I use it in several of my own tools that have nothing to do<br>with the language. I’ve written before about how it manages memory with a tiny arena allocator.<br>This time I want to talk about a much bigger part of the runtime: fibers, the scheduler, the Reactor, and how<br>they let a Serene program do many things at once without tying themselves in knots.

Note: At the time of writing, Serene’s runtime only supports x86_64-linux. Porting it to other platforms and architectures<br>is straightforward, and it’s on my to-do list. Contributions are always welcome.

The problem

Concurrency is one of the hardest1 2 problems in systems programming. Once several independent pieces of work can<br>run at the same time, the number of possible execution orders explodes, and so does the number of bugs hiding inside<br>them. Race conditions, deadlocks, starvation, and subtle ordering bugs are all consequences of the same underlying<br>reality: your program is no longer executing one instruction after another in a predictable sequence.

Fortunately, modern programming languages have spent decades developing abstractions that make concurrent programming<br>far more approachable. Today, most programmers don’t need to think directly about schedulers, event loops, or<br>synchronization primitives every time they write a network service. Those abstractions exist because countless engineers<br>have already wrestled with the underlying complexity and turned it into reusable building blocks.

As someone building a programming language, I don’t get to take those building blocks for granted—I have to choose them.<br>That means understanding the trade-offs each abstraction makes, deciding which ones fit Serene’s goals, and sometimes<br>discovering that none of the existing choices are quite the right fit.

Abstractions and building blocks

The good news is that I don’t have to invent concurrency from scratch. Over the last few decades, people much smarter<br>than me have explored a wide range of concurrency models, each making different trade-offs between simplicity,<br>performance, and flexibility. My job isn’t to create a new abstraction, it’s to choose the one that best fits<br>Serene .

To make those trade-offs concrete, let’s use a web server as an example. Every incoming client represents another unit<br>of work that has to wait for network IO while still allowing the server to make progress on other requests. Different<br>concurrency models solve that problem in very different ways, and the differences become much clearer when they’re all<br>tackling the same workload.

We’ll start with the oldest and most obvious approach.

Threads

A common first step for concurrency is the OS thread: one thread per client, each thread blocks on its own socket, and<br>the kernel does the multiplexing. That model is straightforward and - at small scale - hard to beat. You get real<br>parallel execution on multiple cores without having to build much infrastructure.

But the “one thread per client” approach stops scaling once you move from a handful of clients to many thousands. The<br>pain is cumulative:

Memory footprint (stacks). Every thread needs its own stack. Linux’s default stack size is often around 8 MB, which<br>is fine if the thread is actually working - but for a web server, most connections are idle most of the time. Multiply<br>that default by tens of thousands of threads and you end up reserving enormous amounts of memory for stacks that aren’t<br>doing anything most of the time. The stack size becomes your practical limit on how many clients you can keep alive.

Locking and synchronization (preemption). Threads are preemptive: the kernel can interrupt a thread at any point.<br>That’s good for fairness, but it’s bad for shared-state code. If two threads might touch the same data, they can be<br>interleaved at arbitrary instruction boundaries, so shared access must be guarded. Locks are the price of that safety,<br>and they’re also where complexity, contention, and “why does it deadlock only in production?” bugs tend to appear.

Context switch overhead. Switching between threads requires kernel involvement and comes with real costs:<br>saving/restoring registers and disrupting cache and TLB locality. A single context switch isn’t a big deal; thousands of<br>them per second add up into CPU time spent on orchestration instead of useful work.

Loss of control over scheduling. The kernel...

serene concurrency runtime building blocks time

Related Articles