AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

17 Concurrency / Checkpoint

Checkpoint — Concurrency

25 min · +25 XP · 3 exercises · 14 questions

Exercises0/3
Quiz · 70% to clear14 q

This checkpoint covers the whole module: starting and joining threads, data races and the RAII locks, atomics and compare-exchange, condition variables and the bounded queue, futures, promises and std::async, and the patterns — a thread pool, split-and-reduce, message passing — that keep concurrent programs correct and their output deterministic.

How it works. Fourteen questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.

Before you start, make sure you can answer these from memory:

  • What happens when a joinable std::thread is destroyed, and how does std::jthread differ?
  • Why is an unsynchronised ++counter from two threads undefined behaviour rather than merely inaccurate?
  • When does an atomic replace a mutex, and when does it not?
  • What does compare_exchange_weak do to expected when it fails, and why must it sit in a loop?
  • Why must cv.wait be given a predicate, and what is a lost wake-up?
  • What does std::future::get() do with an exception thrown in the task?
  • Why does a judged program print only after every thread has been joined?

The three programs are a prime census that splits a range across threads and counts with an atomic, a batch of divisions run through std::async where some throw, and a two-stage pipeline of bounded queues whose prefix sums must come out in order. Join everything before you print.

Common questions

Why is ++counter from two threads undefined behaviour?

++counter is a load, an add and a store, and two threads doing it without synchronisation is a data race: one location, at least one write, nothing ordering the accesses. The standard makes that undefined behaviour, so the compiler may assume it never happens — lost increments are only the visible symptom.

What does std::future::get() do with an exception thrown in the task?

It rethrows it. The library catches the exception in the task, stores it in the future's shared state, and get() throws it on the calling thread with its dynamic type intact, so an ordinary try/catch around get() handles errors from another thread.

Why print only after every thread has been joined?

Output written from worker threads interleaves in an order the scheduler decides, so it differs from run to run. Joining every thread first also guarantees the main thread sees everything the workers wrote; it can then print aggregates in a fixed index order that is identical on every run.

C++ Concurrency Quiz: Threads, Mutexes and Atomics Practice

Test your C++ concurrency with 14 questions and three programs on threads, data races, mutexes, atomics, condition variables, futures and std::async.

  • Course: C++ study plan
  • Module: Concurrency
  • Kind: Checkpoint — cleared at 70%
  • Reading time: 25 min
  • Runtime: Clang 18 · C++20

Checkpoint — Concurrency is the checkpoint that closes the Concurrency module: a graded quiz and whole-program exercises, passed at 70%.

Instructions

This checkpoint covers the whole module: starting and joining threads, data races and the RAII locks, atomics and compare-exchange, condition variables and the bounded queue, futures, promises and std::async, and the patterns — a thread pool, split-and-reduce, message passing — that keep concurrent programs correct and their output deterministic.

How it works. Fourteen questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.

Before you start, make sure you can answer these from memory:

  • What happens when a joinable std::thread is destroyed, and how does std::jthread differ?
  • Why is an unsynchronised ++counter from two threads undefined behaviour rather than merely inaccurate?
  • When does an atomic replace a mutex, and when does it not?
  • What does compare_exchange_weak do to expected when it fails, and why must it sit in a loop?
  • Why must cv.wait be given a predicate, and what is a lost wake-up?
  • What does std::future::get() do with an exception thrown in the task?
  • Why does a judged program print only after every thread has been joined?

The three programs are a prime census that splits a range across threads and counts with an atomic, a batch of divisions run through std::async where some throw, and a two-stage pipeline of bounded queues whose prefix sums must come out in order. Join everything before you print.

Common questions

Why is ++counter from two threads undefined behaviour?

++counter is a load, an add and a store, and two threads doing it without synchronisation is a data race: one location, at least one write, nothing ordering the accesses. The standard makes that undefined behaviour, so the compiler may assume it never happens — lost increments are only the visible symptom.

What does std::future::get() do with an exception thrown in the task?

It rethrows it. The library catches the exception in the task, stores it in the future's shared state, and get() throws it on the calling thread with its dynamic type intact, so an ordinary try/catch around get() handles errors from another thread.

Why print only after every thread has been joined?

Output written from worker threads interleaves in an order the scheduler decides, so it differs from run to run. Joining every thread first also guarantees the main thread sees everything the workers wrote; it can then print aggregates in a fixed index order that is identical on every run.

Exercises

A prime census

Read t, lo and hi and count the primes in the inclusive range [lo, hi] with t threads. Split the range into contiguous chunks with the chunk formula over n = hi - lo + 1 numbers (chunk i covers [lo + i * n / t, lo + (i + 1) * n / t)). Each thread counts the primes in its chunk in a local, stores that partial in its own slot, and adds it into a std::atomic<long long> total with one fetch_add. After joining, print chunk <i>: [<a>, <b>) primes <p> for every chunk in index order, then total <count> read from the atomic. is_prime is given; lo is at least 1.

Input: t lo hi. Output: t + 1 lines.

2 1 20

→

chunk 0: [1, 11) primes 4
chunk 1: [11, 21) primes 4
total 8

Divisions through std::async

Read n pairs a b. Launch every division as its own std::async(std::launch::async, ...) task computing a / b (integer division) and throwing std::domain_error("division by zero") when b is 0. Keep the futures in a vector, then get() them in order inside a try: print task <i>: <quotient> or task <i>: error division by zero — the exception crossed the thread boundary through the future and is caught by const std::domain_error&. Finish with ok <k> failed <m>.

Input: n, then n lines a b. Output: n + 1 lines.

4
10 2
7 0
-9 4
0 5

→

task 0: 5
task 1: error division by zero
task 2: -2
task 3: 0
ok 3 failed 1

A two-stage pipeline

BoundedQueue is given, complete, from lesson 4. Build a pipeline of three stages over two queues of capacity 2. Stage 1 (a thread) pushes the n input values into queue A in order, then closes A. Stage 2 (a thread) pops from A until nullopt, keeps a running sum, pushes each prefix sum into queue B, and closes B when A is drained — closing must propagate, or stage 3 waits forever. Stage 3 (the main thread) pops from B into a vector until nullopt. Join both threads, then print prefix <the sums, space-separated> and count <n>. Every queue is FIFO with one consumer, so the prefix sums come out in input order on every run.

Input: n, then n integers. Output: two lines.

5
3 1 4 1 5

→

prefix 3 4 8 9 14
count 5

In this module: Concurrency

← Concurrency patterns — pools, reductions and the deterministic-output rule · Streams in depth — state, recovery and the character API →