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 Makefile — make, 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.