Master the fundamental concepts of threads & concurrency through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksSemaphores generalize mutexes with a counter: wait() decrements (blocking at zero) and post() increments, potentially waking waiters. Kernel semaphores exist, but Linux user-space pthread primitives often sleep on futexes tied to an int guard variable.
Fast path when counter > 0:
FUTEX_WAIT if zero, FUTEX_WAKE on postFor example, a pool of 4 identical buffers uses a semaphore initialized to 4. Each producer wait() consumes a slot; each consumer post() returns one.
Linux's futex(2) syscall, introduced in kernel 2.5.7, is the mechanism glibc's sem_t and pthread_mutex_t are built on today precisely because it avoids a syscall on the uncontended fast path, which is why futex-based locks vastly outperform SysV semaphores under light contention. This same primitive underlies Rust's parking_lot and Java's LockSupport.park(), and the spurious-wakeup handling you implement here is required by the futex(2) man page itself.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will implement sem_wait and sem_post with syscall(SYS_futex, ...) for blocking. Understanding the futex contract matters because incorrect waiter counts produce lost wakeups that TSan cannot see.
Model a counting semaphore built on a futex, the way glibc builds one. sem_wait first tries a fast path in user space (an atomic decrement when the value is positive). Only when the value is 0 does it make the futex_wait(&value, 0) system call. The kernel re-checks that the value is still 0 before it puts the thread to sleep, which is what prevents lost wakeups. sem_post increments the value and calls futex_wake(1) only if somebody is asleep. The input is an explicit interleaving of thread operations, so you can replay exactly the races that make this design necessary.
cLoading…
Threads are created by name on first use.
| Operation | Behaviour |
|---|---|
| wait | Value > 0: take it (fast path). Otherwise the thread is preparing, and its next step must be futex |
| futex | The kernel compares: if value != 0, return EAGAIN and retry at once (take it, or prepare again). Otherwise sleep at the back of the futex queue |
| post | value++. If anyone is asleep, futex_wake(1) moves the oldest sleeper to woken, otherwise no syscall is made |
| run | A woken thread retries: take the value if it is > 0, otherwise it was stolen, and the thread prepares again |
| trywait | Take the value if it is > 0, otherwise EAGAIN. Never sleeps |
A thread that is preparing, asleep or woken cannot wait, trywait or post, and prints T: busy (...) or T: busy, cannot post. futex on a thread that is not preparing prints T: nothing to do (not preparing futex_wait), run on a thread that is not woken prints T: not woken, and an unknown operation prints T: unknown operation X.
cLoading…
The busy messages are (asleep in futex_wait), (woken, must run first) and (preparing futex_wait). state lists sleepers in queue order and the other groups in order of first appearance. The statistics line is always printed last. A successful trywait prints T: trywait succeeded, value N and counts as a fast-path acquisition.
Input:
cLoading…
Output:
cLoading…
futex and post touch the queue, and only futex performs the compare.Hidden tests cover several sleepers woken in FIFO order, stolen wakeups, the lost-wakeup window where a post lands between wait and futex, trywait both ways, busy threads, futex/run at the wrong time, and unknown operations.