Representative interview topic

C++ interview: Implement cooperative cancellation with std::jthread and stop_token

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

Implement a C++20 worker that consumes tasks in a loop while the main thread can request stop. It must not lose a started task, block forever, or access shared state after destruction. Explain std::jthread, stop_token, condition_variable_any, exceptions, and tests.

Prompt and context

Implement a background worker that consumes a queue. During shutdown or upstream cancellation, the main thread requests stop; a started task completes, while unstarted work is either retained or explicitly discarded. Avoid busy waiting, data races, a live thread during destruction, and a condition-variable wait that never returns.

C++20 std::jthread requests stop and joins during destruction, and can inject a std::stop_token into the entry function. A stop request is cooperative; it cannot forcibly terminate arbitrary code. The worker must poll or wait with a stop-aware primitive.

What the interviewer evaluates

  • You understand that a stop token is a request on shared state, not asynchronous thread killing.
  • You use std::jthread automatic joining and keep object members alive while the thread accesses them.
  • You make blocking waits interruptible, for example with the condition_variable_any stop-token overload.
  • You keep queue, stop state, task exceptions, and cleanup lifetimes safe.
  • You test empty-queue stop, stop races, task exceptions, repeated request_stop, and destruction order.

Questions to clarify first

  • May a running task finish, and is it idempotent or externally side-effecting?
  • When the queue closes, are unconsumed tasks dropped, transferred, or drained by another worker?
  • Is the wait interruptible, or does it depend on a third-party I/O call that cannot be cancelled?
  • Are exceptions recorded, propagated to the joining thread, or used to stop the service?
  • Can multiple threads call stop, destruction, or restart?

A 30-second answer

“Own the worker with std::jthread and accept a std::stop_token in its entry function. Protect the queue with a mutex and wait using a stop-aware condition_variable_any; after wake-up check stop, queue closure, and task state. A dequeued task runs to defined cancellation points and cleans up. Destruction requests stop and joins; it never detaches. Shared state outlives the thread. Tests cover stop races and exceptions.”

Step-by-step solution

Step 1: Define the cancellation contract

Separate “stop requested” from “task completed.” A stop request prevents new work; a started task finishes or returns a cancellation result at safe points. Do not promise immediate cancellation of arbitrary third-party calls.

Step 2: Define ownership

Keep the object owning the queue, mutex, condition variable, and std::jthread alive longer than the thread. Destruction requests stop and waits before releasing members. Never capture a reference to a dead scope or publish a raw this to a late callback.

Step 3: Make waits cancellable

Use the condition_variable_any stop-token wait or register a stop_callback that calls notify_all. The predicate checks queue non-empty, closed state, and stop_requested(); wake-ups reacquire the lock and recheck state.

cpp
std::jthread worker([this](std::stop_token st) {
  for (;;) {
    Task task;
    {
      std::unique_lock lock(mu_);
      cv_.wait(lock, st, [this, &st] {
        return closed_ || !queue_.empty() || st.stop_requested();
      });
      if (st.stop_requested() || (closed_ && queue_.empty())) return;
      task = std::move(queue_.front());
      queue_.pop_front();
    }
    run(task, st);
  }
});

Step 4: Handle task stop points and exceptions

Check the token between task phases and define side-effect boundaries to avoid partial writes. Catch exceptions at the thread boundary, record task ID and error, and decide whether to continue or stop the worker. Do not let an exception escape the entry function.

Step 5: Close in the right order

Reject new tasks, mark the queue closed, notify waiters, request stop, join, and only then release resources. Define drain timeout and leftover-task handling. Repeated request_stop() must be safe and must not repeat side effects.

Step 6: Test races and observe behavior

Test empty-queue stop, stop during dequeue, simultaneous stop calls, notification during destruction, task exceptions, blocking-I/O timeout, and repeated close. Record stop latency, completed/cancelled tasks, remaining queue, exceptions, and join time; run ThreadSanitizer for races.

A strong sample answer

“I manage the worker lifetime with jthread and accept stoptoken. A mutex protects queue state; a stop-aware conditionvariable_any checks closed, non-empty, and stop request, so stop wakes the wait. After dequeue I release the lock. The task checks the token at safe points and finishes transaction cleanup. The entry catches and records exceptions.”

“Shutdown rejects new work, sets closed, notifies, requests stop, and joins. It never detaches or releases the queue and logger early. Tests cover an empty queue, dequeue races, exceptions, repeated stop, long-task timeout, and ThreadSanitizer.”

Common mistakes

  • Treat stop_token as forced killing → resources and transactions break → define cooperative points.
  • Forget to join a std::thread termination or a dangling thread → use jthread or explicit lifetime ownership.
  • Wait for one notification → a missed notification sleeps forever → use a predicate loop and wake on stop.
  • Run tasks while holding the lock → producers and shutdown block → release after dequeue.
  • Let exceptions escape the entry → the process terminates → catch at the thread boundary.
  • Release members before stopping the thread → use-after-free → stop, join, then release state.

Follow-up questions and answers

What does a jthread destructor do?

If it is joinable, destruction requests stop and joins; it does not forcibly terminate the task. The task must respond, so join may wait for a safe point.

Can a stop request wake a condition variable?

The stop-token overload of condition_variable_any returns when stop is requested. A custom wait needs a stop callback to notify and a predicate that checks state again.

Can an in-flight database write be cancelled immediately?

Do not assume so. Use rollbackable or idempotent steps, the driver’s timeout/cancellation support, and a stop check at commit boundaries.

How do you avoid stop races?

Treat closed, queue, and stop as one lifecycle protocol. Perform transitions under the lock and notify after state changes; stress the window where stop and dequeue happen together and run ThreadSanitizer.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool