Phase 3 Projects

Three projects. Each one produces a public artifact that hiring managers can read in under 10 minutes and get a signal from. Ordered by dependency: SPSC first because you need to trust std::atomic; thread pool next because you need packaged_task + a queue; event loop last because it composes both.

Time budget: 6 weeks of build time across W22-W28, roughly 30-40 hours per project. If you exceed 50 hours on any single project, freeze scope and ship what you have. A shipped 70% project beats an unshipped 100% one in every hiring conversation you will ever have.


P3.1 — SPSC Lock-Free Ring Buffer (W22-W23)

Goal. Implement a single-producer / single-consumer bounded queue using std::atomic with cache-line-aware layout. Benchmark it against boost::lockfree::spsc_queue. Prove correctness under -fsanitize=thread on 1M+ operations.

Deliverable. GitHub repo raghul-r/spsc-queue with:

  • include/spsc_queue.hpp — the header from file 04, cleaned up, documented.

  • bench/bench_spsc.cpp — a driver that pushes 10M uint64_t items across cores 0 and 1 and reports throughput in items/sec and ns/op.

  • bench/bench_boost.cpp — the same driver against boost::lockfree::spsc_queue.

  • test/test_spsc.cpp — GoogleTest suite: empty, full, wrap-around, N-item burst, N-item interleaved, destructor with items still in queue.

  • .github/workflows/ci.yml — three build jobs: release, asan-ubsan, tsan. All three must be green.

  • README.md — build instructions, benchmark table, one plot (matplotlib is fine), and a “known limitations” section.

Acceptance criteria (all must hold).

#

Criterion

How you verify

1

Compiles clean with -std=c++20 -Wall -Wextra -Wpedantic -Werror on clang 18+ and gcc 13+

CI green

2

Passes 1,000,000-item push/pop under -fsanitize=thread with zero warnings

CI job tsan

3

Zero heap allocations per push/pop after construction

Instrument with a custom operator new counter test

4

Throughput within 2× of boost::lockfree::spsc_queue on Apple M-series

Bench table in README

5

Correctly handles a T with non-trivial destructor (use std::string in one test)

Test case + ASan clean

6

Cache-line alignment verified with static_assert(alignof(...) == 128) on Apple Silicon

Compile-time check in header

Traps to expect.

  • You will forget to loop the CAS/store on ARM at first. TSan will scream. Fix by making the store memory_order_release.

  • Your first benchmark will look slower than Boost because you compiled at -O0. Rerun with -O3 -march=native. On Apple Silicon use -mcpu=apple-m1 or apple-m2native misses some flags on Clang for cross-arch reasons.

  • hardware_destructive_interference_size may be missing on older libstdc++ setups. Fall back to a constexpr 128 with a warning.

  • You will benchmark on a machine also running Slack, VS Code, and 47 Chrome tabs. Numbers will be noisy. Pin the benchmark to specific cores with taskset on Linux; on macOS use xctrace record --template 'CPU Profiler' and read the “on-CPU” percentages. Run each measurement 10 times; report median and MAD, not mean.

Publish location. github.com/<you>/spsc-queue + a companion post on dev.to or your blog titled “Writing a 100M items/sec SPSC queue in modern C++”. Cross-post to r/cpp after the second week (Monday morning UTC gets the most engagement).

Employer signal.

  • HFT firms (Optiver, Jane Street, Jump, IMC): direct portfolio piece. They will read the code.

  • Systems roles at FAANG / infra roles at Zoho / any low-latency team: proves you can reason about memory ordering, not just quote it.

  • ML infra roles: same skills apply to feature-store pipelines, inference queueing.


P3.2 — Thread Pool with Futures (W24-W26)

Goal. A ThreadPool class with submit(callable, args...) -> future<result>, work-stealing optional, graceful shutdown, no leaks under sanitizer. This is the workhorse of every real C++ codebase; study partners ask for it constantly.

Deliverable. GitHub repo raghul-r/thread-pool with:

  • include/thread_pool.hpp — single-header, no dependencies beyond stdlib.

  • test/test_pool.cpp — GoogleTest covering: 100K trivial tasks; task that returns void; task that throws; task that returns unique_ptr<T>; destructor with tasks still pending; submit after shutdown returns a broken future.

  • bench/bench_pool.cpp — 100K empty tasks, 10K tasks each doing 1 µs of work, and a scaling curve (1..N cores).

  • .github/workflows/ci.yml — same three configurations as P3.1.

  • README.md — API, one-page sequence diagram of shutdown, benchmark table.

Public API (target).

class ThreadPool {
public:
    explicit ThreadPool(std::size_t n = std::thread::hardware_concurrency());
    ~ThreadPool();                          // joins remaining workers

    ThreadPool(const ThreadPool&)            = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    // Submit any callable; returns a future for the result type.
    template <typename F, typename... Args>
    auto submit(F&& f, Args&&... args)
        -> std::future<std::invoke_result_t<F, Args...>>;

    // No more submissions accepted; drains queue then joins.
    void shutdown();

    // Drops pending tasks and joins ASAP.
    void shutdown_now();

    std::size_t size() const noexcept;      // worker count
    std::size_t queue_depth() const noexcept;
};

Internal structure:

  • One mutex-protected std::deque<std::function<void()>> for the task queue (upgrade to per-worker deques with work stealing as a v2).

  • One std::condition_variable for worker wake-up.

  • One std::atomic<bool> stop_ for shutdown.

  • Each worker is a std::jthread that loops: wait_for_task() run repeat.

  • Submission builds a std::packaged_task<R()>, moves it into the queue, returns its future.

Acceptance criteria.

#

Criterion

How you verify

1

100,000 trivial tasks ([]{return 1;}) complete in under 500 ms on Apple M-series

Bench output

2

Zero errors under -fsanitize=thread on the full test suite

CI job tsan

3

Zero errors under -fsanitize=address,undefined

CI job asan-ubsan

4

Destructor never hangs, even if tasks are still submitted

Test case + timeout in CI

5

Exceptions thrown by tasks are captured and re-thrown in the future

Test case

6

Scaling curve is monotonic up to hardware_concurrency() (within noise)

Bench plot

7

Header compiles standalone (no .cpp file) with -std=c++20

Compile check

Traps.

  • Destructor deadlock. If your destructor holds the queue mutex while waiting for workers to notice stop_, and a worker is trying to lock the same mutex to pop a task, you deadlock. Order: set stop_, notify_all outside the lock, then join.

  • Submit-during-destructor. Decide upfront: either submit after shutdown() throws, or returns a future that resolves to broken_promise. Document it.

  • Task holds a reference to a local, but the local goes out of scope. Not your bug to fix, but document it in the README: “lifetimes of task captures are the caller’s responsibility.”

  • std::function allocates. For tiny tasks this shows up as ~30% overhead in the 100K-trivial-tasks bench. If you care, add a small-buffer-optimized move_only_function (C++23) or roll your own. Note it as a future optimization; don’t rabbit-hole in v1.

  • False sharing on queue_depth_ counter. If you expose a size() atomic and workers all decrement it, it becomes a contention hotspot. Consider a stale-but-cheap approximation.

Publish location. github.com/<you>/thread-pool. Post a walkthrough on dev.to or your blog: “A production-quality thread pool in 300 lines of modern C++”.

Employer signal.

  • Every C++ shop uses a thread pool. Being able to write one from memory is the equivalent of being able to write quicksort. Zoho, FAANG, HFT, all of them.

  • Bonus signal: your shutdown semantics table (graceful vs immediate) shows you think about lifecycles, not just correctness. That’s senior-level.


P3.3 — Minimal Event Loop with kqueue (W27-W28)

Goal. A single-threaded echo server that handles 1,000 concurrent connections using kqueue on macOS (with epoll fallback notes for Linux). No threads. No coroutines. Just fds and an event loop.

Deliverable. GitHub repo raghul-r/mini-event-loop with:

  • src/event_loop.hpp / .cpp — a small event loop abstraction wrapping kqueue.

  • src/echo_server.cpp — a program that opens a TCP listen socket, accepts connections, and echoes bytes back until close.

  • bench/loadtest.sh — invokes ab -c 100 -n 10000 http://127.0.0.1:8080/ (or wrk — either works) and checks for zero non-2xx responses.

  • README.md — architecture diagram, syscall trace excerpt, latency histogram, Linux port notes.

Design constraints.

  1. One thread. If you spawn a thread, you failed the exercise.

  2. All sockets are non-blocking (fcntl(fd, F_SETFL, O_NONBLOCK)).

  3. Edge-triggered with EV_CLEAR. You must drain to EAGAIN on every event.

  4. Backpressure: if you can’t write everything, register EVFILT_WRITE on that fd, buffer the pending bytes, and resume on the next writable event. Do not busy-spin.

  5. Clean shutdown: SIGINT via EVFILT_SIGNAL triggers loop exit. No stuck sockets on Ctrl-C.

Acceptance criteria.

#

Criterion

How you verify

1

ab -c 100 -n 10000 completes with zero failures

Log output

2

Peak concurrent connections ≥ 1,000

Custom load script (or hey -c 1000 -n 10000)

3

Zero EAGAIN-loop hangs (fd registered for read but no drain loop)

Code review checklist + one adversarial test

4

Under -fsanitize=address,undefined, load test still passes

CI

5

Server exits cleanly on SIGINT, no leaked fds (lsof diff = 0)

Manual test in README

6

Latency: median echo round-trip < 200 µs on localhost (M-series)

Custom timing script; log to CSV

Traps.

  • Edge-triggered without a drain loop. Data is silently dropped. Symptom: works at low concurrency, fails intermittently at high. Fix in file 05 §3.

  • Assuming accept() returns one connection per readable event. With EV_CLEAR you must accept in a loop until it returns EAGAIN.

  • EPIPE / SIGPIPE on write to a closed peer. On macOS use setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, ...), or send(fd, buf, n, MSG_NOSIGNAL) (Linux). Without this a client Ctrl-C will kill your server.

  • Fd leaks. Every close() path must be exhaustive: error branches, EOF, peer reset, shutdown. Track it with an atomic counter in DEBUG builds and assert zero at exit.

  • Buffering per connection. You need one send-buffer and one recv-buffer per fd, keyed off the fd. A std::unordered_map<int, ConnState> is fine for a first version; profile before optimizing.

  • Testing only with curl. curl opens one connection at a time. Use ab, wrk, or hey for concurrency.

Linux port notes (put in the README, not required to run).

  • Swap kqueue/kevent for epoll_create1/epoll_ctl/epoll_wait.

  • EV_CLEAREPOLLET.

  • EVFILT_SIGNALsignalfd.

  • SO_NOSIGPIPEMSG_NOSIGNAL on each send.

  • Everything else is byte-identical.

If you have a Linux VM available, do the port. If not, document it and move on. Being able to reason about the port is 80% of the signal; actually running it is the last 20%.

Publish location. github.com/<you>/mini-event-loop. Blog: “A 500-line echo server that handles 1,000 connections on one thread”.

Employer signal.

  • Backend / infra / distributed systems roles: this is the “do you understand what a server is” question, answered as a project.

  • Networking C++ (Cloudflare, Cisco, Cilium/Isovalent) or trading gateway roles: this is a warm-up. Extend to HTTP/1.1 parsing if you’re studying there.

  • Zoho: their entire platform runs on servers of this shape. Reads well.


Cross-cutting rules for all three projects

  • README first, code second. Draft the README before you write the first line. If you can’t explain what “success” looks like, you can’t build it.

  • Commit small. Every commit should be reviewable in under 5 minutes. Squash before publishing if needed.

  • Sanitizers on day one. Add ASan/UBSan to CI in the first commit, not the last. Retro-fitting sanitizers to a broken codebase is punishing.

  • Benchmarks are code. Version them. Commit the raw numbers. study partners who care will ask for reproducibility; a bench/run.sh that regenerates the table saves the conversation.

  • Publish or perish. An unpublished project is worth zero career points. Push to GitHub. Post to r/cpp. Add to your resume. If it doesn’t have a URL, it doesn’t count.

What you deliver at the end of Phase 3

By W28 you should have three green GitHub repos, three blog posts, and one thing you can say in an study without hedging: “I understand what my code compiles to.” If you can say that and back it up with SPSC + thread pool + event loop, you’re through the systems-engineer bar at every C++ shop on the planet.


Nav: ← 06 Undefined Behavior · Phase 3 README · Master README · → Phase 4 Applied C++