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 worksA data race occurs when two threads access the same memory location, at least one write is present, and no synchronization orders them. The C memory model calls this undefined behavior; observed output can change run to run.
Compile with -fsanitize=thread:
For example, counter++ without atomics from two threads may report a race at the increment line even if you usually see 1000 as the final value.
ThreadSanitizer, the tool this task exercises, was built at Google after data races caused real, hard-to-reproduce production incidents in Chrome's multi-process renderer and is now standard in Go's -race flag and Rust's Miri. The double-checked-locking bug shown here is a famous real pattern (formalized as broken by the 'C++ and the Perils of Double-Checked Locking' paper) that misled a generation of C++ programmers into shipping racy singleton initialization.
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 write a deliberately racy program, run it under TSan, and apply fixes with mutexes or atomics. This exercise requires pasting the TSan report and explaining which invariant the fix restores.
ThreadSanitizer finds data races without needing the unlucky timing to happen. It tracks happens-before with vector clocks and flags two accesses to the same variable (at least one a write) that are not ordered by any synchronisation. Build that detector. The input is a trace of memory accesses and synchronisation operations, and the output lists every race.
cLoading…
A thread mentioned for the first time starts with its own component at 1 and all others at 0, as if created at program start. Each thread's clock is printed with one entry per known thread, in order of first appearance.
| Operation | Clock update |
|---|---|
unlock M | L[M] = C[T], then C[T][T]++ |
lock M | C[T] = max(C[T], L[M]) (element-wise; a never-released lock is all zeros) |
fork U | C[U] = max(C[U], C[T]), then C[T][T]++. Print U's clock |
join U | C[T] = max(C[T], C[U]), then C[U][U]++. Print T's clock |
Race checks. For each variable, remember the last write as (thread u, value C[u][u] at the write), and for each thread the value of its own component at its latest read since that write. An earlier access by thread u at value c happens-before the current access by T if c <= C[T][u].
cLoading…
An unknown operation prints T: unknown operation OP. The last line always counts races and distinct racy variables.
Input:
cLoading…
Output:
cLoading…
Hidden tests cover concurrent reads (no race), a write racing with several readers, two different locks (which do not order each other), message passing through one lock, ordering through join, threads that are never forked, and unknown operations.