What it did
Capstone of the C++ nanodegree’s concurrency module. Simulate a city
where each vehicle is a thread, each intersection is a thread, each
traffic light is a thread, and they coordinate via a templated
MessageQueue<T> blocking on std::condition_variable. Render the
simulation in real-time via OpenCV’s window API.
The architecture
Vehicleowns its own thread; runs a loop that drives along a street + waits at intersections.Intersectionowns its own thread; receives vehicle entries via aMessageQueue<Vehicle>; lets the next vehicle in when itsTrafficLightpermits.TrafficLightowns its own thread; cycles red → green on a random ~4–6 second interval; broadcasts the current phase via a templatedMessageQueue<TrafficLightPhase>.MessageQueue<T>is the synchronization primitive — a thread-safe deque withsend()andreceive()blocking the caller until data is available.
What was actually tricky
- Spurious wakeups.
cv.wait(lock)can wake up without notify; always wrap inwhile (queue.empty()) cv.wait(lock). - Owning vs. weak references. A
std::shared_ptr<Intersection>held by both aVehicleand aStreetcreates ref cycles.std::weak_ptrfor back-references resolves it. - OpenCV drawing is not thread-safe. All rendering had to happen on a single thread; vehicle threads pushed pose updates into a shared buffer that the render thread read.
What I’d do differently with hindsight
- Avoid threading per actor. N=100 vehicles = 100 OS threads, which the kernel scheduler hates. A coroutine-based design (or even a single thread + state machine per actor) scales much better.
std::stop_tokenfor cancellation (C++20). The project ended up with manualbool stopRequestedflags; the standard’s cancellation primitives are cleaner.- Async messaging via
boost::asioor libuv if the goal was realism. The hand-rolledMessageQueue<T>is great pedagogy and terrible production code.
What it taught me
Concurrency in C++ is mostly about contracts: which thread owns
which data, who can read what when. The actual mutex and
condition_variable are mechanical once the ownership graph is
clear. Get the ownership wrong and you’ll spend hours chasing
races no debugger can catch. Hours which I did, in fact, spend.