Quant developer HFT course · Module 4: Concurrency
Drill: concurrency
Concurrency questions reward precision. Name the shared state, name the synchronisation, name the ordering, and say how you would prove it with a tool.
Prompt 1: The short count
The interviewer says
Two threads do ++count on a shared int and the total comes out low. Explain and fix.
Before you answer
Question · choose every correct option
Which statements are correct? Choose all that apply.
volatile gives no atomicity and no ordering. The atomic increment does; per-thread counters avoid the shared line altogether.
Answers that lose the point
Use volatile.
Still a race, still lost increments.Put a mutex around it.
Correct, but heavier than needed for a counter.
A strong answer
++count is three steps, load, add, store, and two threads can both load the same value, so one increment is lost. In C++ it is also a data race, which is undefined behaviour, so the damage is not limited to the count.
For a pure counter I would use std::atomic<uint64_t> with fetch_add and relaxed ordering, since nothing else is published through it. On a hot path I would go further and give each thread its own counter on its own cache line, summed when read, so the threads never contend.
Prompt 2: The flag that never flips
The interviewer says
A worker loops while(!stop) and another thread sets stop = true. At -O2 the worker never exits. Why?
Before you answer
Question · choose every correct option
What is going on? Choose all that apply.
Caches are coherent; the write does arrive. The problem is that the compiler never reads it again.
Answers that lose the point
The CPU cache is stale.
Coherence protocols keep caches consistent; the compiler is the culprit.
A strong answer
The flag is a plain bool shared between threads, which is a data race and undefined behaviour. With no synchronisation, the optimiser may assume nothing else changes stop inside the loop and read it once, so the loop never ends.
Making it std::atomic<bool> forces a real load on every iteration. If the flag also signals that some data is ready, the store should be release and the load acquire; for a pure stop request, relaxed would do, though I would default to the stronger orders unless profiling says otherwise.
Prompt 3: Busy days are slow
The interviewer says
Strategy and gateway threads share a mutex-protected book. Latency is fine when quiet and bad when busy. What do you investigate?
Before you answer
Question · choose every correct option
What belongs in the answer? Choose all that apply.
Contention grows with load; the cure is shorter critical sections or no shared lock at all.
Answers that lose the point
Use a faster mutex.
The cost is waiting, not the lock instruction.Add more threads.
More threads, more contention.
A strong answer
That pattern points at contention. An uncontended mutex costs a couple of atomics, but a contended one puts the waiting thread to sleep in the kernel, and waking it takes microseconds with a long tail. Busier days mean more overlap.
I would measure how long the lock is held and look for slow work inside it: logging, allocation, I/O, computations that could run on a copy. Shortening the critical section helps immediately. The structural fix is to give the book a single owning thread and have the other thread send it requests and receive snapshots through lock-free queues, so there is no shared lock.
Prompt 4: Publishing a quote
The interviewer says
The feed thread writes a quote into a buffer and sets a flag; the strategy thread spins on the flag and reads the quote. Which memory orders?
Before you answer
Question · choose every correct option
Which are correct? Choose all that apply.
Release-acquire is the publication contract. Relaxed may happen to work on x86 hardware but lets the compiler reorder, and fails on ARM.
Answers that lose the point
seq_cst everywhere.
Correct and defensible, but you should be able to say why release-acquire suffices.Relaxed, it's x86.
The compiler is still free to reorder; the code becomes non-portable.
A strong answer
Release on the store of the flag and acquire on the load. The release guarantees that the write of the quote is visible no later than the flag, and the acquire guarantees the strategy reads the quote only after seeing the flag. Together they synchronise, so seeing the flag means seeing the quote.
On x86 both compile to ordinary moves and the orders only restrict the compiler; on ARM they emit the needed barriers. seq_cst would also be correct but is stronger than this pattern requires, and on x86 a seq_cst store costs a fence.
Prompt 5: The fastest handoff
The interviewer says
Design the queue from the feed thread to the strategy thread. One producer, one consumer, minimum latency.
Before you answer
Question · choose every correct option
Which design choices are right? Choose all that apply.
The ring is wait-free and allocation-free; padding and cached indices keep the shared lines quiet; spinning avoids kernel wake-ups.
Answers that lose the point
A mutex and a condition variable.
Correct, but every handoff can go through the kernel.A lock-free MPMC queue from a library.
Works, but pays for competing producers that do not exist here.
A strong answer
A single-producer single-consumer ring over a preallocated array with a power-of-two capacity. The producer writes a slot and stores the tail with release; the consumer loads the tail with acquire, reads the slot, and stores the head with release, which the producer reads with acquire before reusing the slot. No locks, no compare-and-swap, no allocation.
For speed I would put head and tail on separate cache lines, cache each side's view of the other's index, construct large messages in place, and run the consumer on an isolated core polling the ring rather than sleeping. When the ring is full, a feed producer drops and flags a gap rather than blocking.
After the drill
Every concurrency answer has the same skeleton: who writes, who reads, what orders the accesses, and what happens under contention. Module 5 goes underneath all of this to the caches and cores that make the costs what they are.