all work

shipped·systems·2025

Scalable thread management library

A C and POSIX-threads library with lifecycle control, four synchronisation primitives, a priority thread pool with cancellation and auto-scaling, and a live dashboard reading real metrics off the running binary.

throughput
193.8tasks/sec
sequential vs pool speedup
1.77×
test suite
51 / 51

The actual problem

Threads are taught as an API — pthread_create, pthread_join, a mutex — and the parts that actually decide whether a concurrent program works are the ones that never appear in the lecture: what happens to a queued task nobody wants any more, how a pool sized for average load behaves under a spike, and how you see any of it while it runs.

This library is built around those three questions.

What it does

Three modules with a clean boundary. Lifecycle management (create, join, detach, cancel, status), a synchronisation engine (mutex, semaphore, barrier, read-write lock), and a thread pool with a priority queue over NORMAL / HIGH / CRITICAL.

ThreadPool *p = pool_init_ex(4, 2, 16, /* auto_scale */ 1);
task_submit_priority(p, work, arg, id, PRIORITY_CRITICAL, "reindex");
task_cancel(p, id);          /* only while still queued */
pool_resize(p, 8);           /* at runtime */

Cancellation and resizing at runtime. A queued task can be cancelled by id before it starts, and the pool can grow or shrink while work is in flight. In auto-scaling mode a monitor thread adjusts the size against observed load, which is the case the fixed-size version handles badly.

Observability, because concurrency bugs are invisible otherwise. Metric snapshots every 250 ms feed a WebSocket bridge and a browser dashboard: a thread grid, live terminal output from the real binary, and four Chart.js series — throughput, completed, queue depth, live threads. The same series export to CSV and JSON.

Measured on the demo workload:

tasks submitted     1,130
tasks completed     1,125
tasks cancelled         5
peak live threads      16
throughput          ~193.8 tasks/sec
average exec time    64.76 ms
sequential vs pool     1.77× speedup

The 1.77× is the number worth being honest about. With sixteen threads a naive reading expects far more; the workload is short-lived tasks, so scheduling, mutex contention on the queue, and the sequential portion dominate — Amdahl’s law measured rather than quoted. A benchmark that reported the speedup without that caveat would be the more impressive and less useful result.

Engineering notes

The logger is thread-safe with levels, timestamps and thread ids, writing to both stdout and a file. That sounds like a small feature and it is the reason the rest could be debugged at all — interleaved printf from sixteen threads produces something unreadable, and worse, its own locking changes the timing you are trying to observe.

51 tests across lifecycle, all four primitives, the pool, priority ordering, metrics, cancellation, dynamic resize, the logger, CSV export, and a scalability case that pushes 500 tasks through. The cancellation and resize tests are the ones that found real bugs, both in the same place: the boundary between “queued” and “running”, where a cancel arriving a microsecond late must be a no-op rather than a use-after-free.

Build is a plain Makefilemake, make run, make test, make graphs, make server.

What I would change

Metrics are captured on a fixed 250 ms tick, which is fine for a dashboard and too coarse for tail latency — a per-task histogram would let me report p95 and p99 wait times instead of an average that hides them. I would also like the auto-scaler to have a documented policy with hysteresis, rather than the current threshold rule that can oscillate when load sits exactly on the boundary.

Keep scrollingCredentials