Threads, synchronization, and the memory model
Data races, happens-before, bounded workers and exception transport
A data race occurs when conflicting accesses to the same memory location happen concurrently without synchronization and at least one writes. It is undefined behavior, not merely a chance of losing increments. volatile does not make an increment atomic or publish neighboring data. Protect a multi-field invariant with one mutex spanning the entire read-modify-write operation.
std::mutex mutex;
Int accepted=0;
// Every access to accepted must obey the same locking rule.
{ std::lock_guard lock(mutex); ++accepted; }A mutex unlock synchronizes with a later successful lock on the same mutex, creating a happens-before relationship. Condition-variable waiting releases the lock while waiting and reacquires it before checking state. Always wait with a predicate: wakeups can be spurious and a notification is not a retained item of work. The shared predicate is the state; the notification only prompts a recheck.
Atomics provide race-free operations on one object. Relaxed ordering is enough to assign unique work indices with fetch_add when it is not publishing unrelated state. Release/acquire can publish prior writes across threads, but requires a matching protocol; sprinkling atomic flags among unsynchronized containers does not make the containers safe. Independent vector elements can be written by distinct workers after sizing the vector in advance; concurrently push_back-ing to the same vector is different and unsafe.
std::atomic<std::size_t> next{0};
auto index=next.fetch_add(1,std::memory_order_relaxed);
// index uniquely assigns an already allocated output slot.A joinable std::thread destructor terminates the process, so every started thread needs a join on every exit path, including partial thread-creation failure. std::jthread can simplify lifetime management on supported libraries, but this exercise uses portable std::thread. Exceptions escaping a thread function also terminate; capture an exception_ptr, finish joining, and rethrow on the calling thread. Join makes completed worker writes visible to the caller.
Bound concurrency independently of input size. One thread per element can exhaust resources. The final API requires a bounded worker pool and preserves result order by assigning output slots by input index rather than completion order. A callback failure aborts the returned operation, but already started callback side effects cannot be rolled back; document that limit rather than promising a transaction across arbitrary code.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.