A 20-question C++ concurrency interview set, progressing from fundamentals to more advanced topics.
1. What is a race condition in C++?
A race condition occurs when multiple threads access shared data concurrently and the program's behavior depends on the timing of those accesses.
If multiple threads access the same memory location, at least one access is a write, and there is no proper synchronization, it can be a data race, which results in undefined behavior.
cpp
int counter = 0;
void increment() {
++counter; // Unsafe if called concurrently
}
Use a mutex:
cpp
std::mutex m;
void increment() {
std::lock_guard<std::mutex> lock(m);
++counter;
}
Interview takeaway: A data race is not merely a logical bug—it is undefined behavior according to the C++ standard.
───
2. What is the difference between std::mutex and std::atomic?
std::mutex protects a critical section, while std::atomic provides atomic operations on an individual object.
cpp
std::atomic<int> counter{0};
counter.fetch_add(1);
A mutex:
cpp
std::mutex m;
{
std::lock_guard<std::mutex> lock(m);
++counter;
}
Use an atomic when the operation you need can safely be expressed through atomic operations.
Use a mutex when you need to protect:
* Multiple variables
* A complex data structure
* An invariant involving several operations
Interview takeaway: Atomic doesn't mean "faster in every situation." Choose based on the synchronization problem you're solving.
───
3. What is a deadlock? How can you prevent it?
A deadlock occurs when threads wait indefinitely for resources held by each other.
For example:
text
Thread 1: locks A → waits for B
Thread 2: locks B → waits for A
One solution is consistent lock ordering.
A better option when locking multiple mutexes is:
cpp
std::scoped_lock lock(mutexA, mutexB);
Other prevention techniques:
* Always acquire locks in a consistent order.
* Keep critical sections small.
* Avoid unnecessary nested locking.
* Use RAII lock types.
* Use std::scoped_lock for multiple mutexes where appropriate.
───
4. What is the difference between std::lock_guard and std::unique_lock?
Both provide RAII-based mutex ownership.
lock_guard
Simple and lightweight:
cpp
std::lock_guard<std::mutex> lock(m);
It locks immediately and unlocks automatically when it goes out of scope.
unique_lock
More flexible:
cpp
std::unique_lock<std::mutex> lock(m);
It supports:
* Deferred locking
* Unlocking and relocking
* Moving ownership
* Conditional locking
* std::condition_variable
For example:
cpp
std::unique_lock<std::mutex> lock(m);
cv.wait(lock);
Interview takeaway: Use lock_guard for simple locking; use unique_lock when you need additional control or a condition variable.
───
5. What is a condition variable?
A std::condition_variable allows a thread to efficiently wait until some condition becomes true.
Example:
cpp
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] {
return !queue.empty();
});
Another thread can notify it:
cpp
{
std::lock_guard<std::mutex> lock(m);
queue.push(value);
}
cv.notify_one();
While waiting, the condition variable temporarily releases the mutex, allowing another thread to acquire it.
Interview takeaway: Condition variables are commonly used to implement producer-consumer queues.
───
6. What is a spurious wakeup?
A spurious wakeup means a thread waiting on a condition variable can wake up even though the desired condition isn't true.
Therefore, this is unsafe:
cpp
cv.wait(lock);
use(queue.front());
Instead:
cpp
cv.wait(lock, [] {
return !queue.empty();
});
Conceptually, the second form does:
cpp
while (!queue.empty()) {
// ...
}
More accurately:
cpp
while (!condition) {
cv.wait(lock);
}
Interview takeaway: Always use a predicate with wait() unless you have a specific reason not to.
───
7. What is the difference between std::async, std::thread, and std::jthread?
std::thread
Represents a thread directly:
cpp
std::thread t([] {
do_work();
});
t.join();
You manage its lifetime yourself.
std::async
Runs an asynchronous task and provides a future:
cpp
auto f = std::async(std::launch::async, [] {
return 42;
});
int result = f.get();
std::jthread
Introduced in C++20. It provides RAII thread management and cooperative cancellation.
cpp
std::jthread t([] {
do_work();
});
A jthread automatically joins when destroyed.
Feature
thread
async
jthread
Represents thread
Yes
Not directly
Yes
Returns result
No
Yes
No
Auto-join
No
N/A
Yes
Cancellation support
Manual
No direct mechanism
stop_token
───
8. What are std::future and std::promise?
They provide a mechanism for transferring a result from one thread to another.
Producer:
cpp
std::promise<int> promise;
auto future = promise.get_future();
std::thread t([&promise] {
promise.set_value(42);
});
Consumer:
cpp
int result = future.get();
Conceptually:
text
Thread A
|
| set_value(42)
v
promise
|
v
future
|
v
Thread B
|
| get()
v
42
They can also transfer exceptions.
───
9. What is the C++ memory model and what does "happens-before" mean?
The C++ memory model defines how threads interact with shared memory and what ordering guarantees synchronization operations provide.
A happens-before relationship establishes that one operation is ordered before another in the C++ memory model.
For example:
cpp
// Thread 1
{
std::lock_guard lock(m);
data = 42;
}
// Thread 2
{
std::lock_guard lock(m);
std::cout << data;
}
The unlock performed by Thread 1 synchronizes appropriately with a subsequent lock acquired by Thread 2, so Thread 2 can safely observe the protected state.
Interview takeaway: Understanding happens-before is essential for understanding why mutexes and atomics make communication between threads safe.
───
10. What are C++ atomic memory orders?
Common memory orders are:
cpp
std::memory_order_relaxed
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst
Relaxed
Provides atomicity but minimal ordering guarantees.
Acquire
Ensures appropriate subsequent operations aren't moved before the acquire.
Release
Ensures appropriate preceding operations aren't moved after the release.
Acquire-release
Combines both.
Sequentially consistent
The strongest commonly used ordering and the default for many atomic operations.
Example:
cpp
std::atomic<bool> ready{false};
int data = 0;
// Producer
data = 42;
ready.store(true, std::memory_order_release);
// Consumer
if (ready.load(std::memory_order_acquire)) {
std::cout << data;
}
The release/acquire pair establishes the required synchronization.
───
Advanced C++ Concurrency
11. What is false sharing?
False sharing occurs when different threads modify different variables that happen to reside on the same CPU cache line.
For example:
cpp
struct Data {
std::atomic<int> a;
std::atomic<int> b;
};
Thread 1 repeatedly updates a, while Thread 2 repeatedly updates b.
Although they don't logically share data, the CPU cache line containing both variables may continually bounce between cores.
This can severely hurt performance.
A common solution is to separate frequently modified variables onto different cache lines, using techniques such as padding or:
cpp
alignas(std::hardware_destructive_interference_size)
std::atomic<int> a;
Interview takeaway: False sharing is a performance problem, not normally a correctness problem.
───
12. What is lock-free programming?
Lock-free programming uses atomic operations instead of traditional mutex-based locking.
Example:
cpp
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);
The goal is to allow threads to make progress without blocking on a mutex.
Important distinction:
* Lock-free: at least one thread can make progress.
* Wait-free: every thread can complete its operation within a bounded number of steps.
* Obstruction-free: a thread can make progress if it executes in isolation.
Interview takeaway: Lock-free does not automatically mean faster or easier to write correctly.
───
13. What is std::call_once used for?
std::call_once ensures that a particular initialization function executes only once, even when multiple threads call it concurrently.
cpp
std::once_flag flag;
void initialize() {
std::call_once(flag, [] {
// Initialization happens once
});
}
Even if 100 threads call initialize(), the initialization code executes only once.
It's useful for thread-safe one-time initialization.
Interview takeaway: In modern C++, a function-local static is also thread-safe for initialization:
cpp
Singleton& instance() {
static Singleton s;
return s;
}
───
14. How does a thread pool work?
A thread pool maintains a fixed number of worker threads and a queue of tasks.
Conceptually:
text
+----------------+
submit() ---> | Task Queue |
+-------+--------+
|
+-----------+-----------+
| | |
Worker 1 Worker 2 Worker 3
Typical implementation:
1. Create N worker threads.
2. Store tasks in a synchronized queue.
3. Workers wait on a condition variable.
4. When a task arrives, notify a worker.
5. Worker removes and executes the task.
6. Repeat until shutdown.
The main advantage is avoiding the overhead of repeatedly creating and destroying threads.
───
15. How would you implement a thread-safe queue?
A simple mutex + condition-variable implementation looks like:
cpp
template<typename T>
class ThreadSafeQueue {
std::queue<T> queue;
mutable std::mutex mutex;
std::condition_variable cv;
public:
void push(T value) {
{
std::lock_guard<std::mutex> lock(mutex);
queue.push(std::move(value));
}
cv.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [this] {
return !queue.empty();
});
T value = std::move(queue.front());
queue.pop();
return value;
}
};
Important considerations in production code include:
* Shutdown handling
* Exceptions
* Move semantics
* Returning std::optional<T> where appropriate
* Avoiding unnecessary copies
* Supporting cancellation
Interview takeaway: The mutex protects the queue; the condition variable prevents workers from busy-waiting.
───
16. What is the ABA problem?
The ABA problem commonly appears in lock-free algorithms using compare-and-exchange.
Imagine:
text
Initial: A
Thread 1 reads A
↓
Thread 2 changes A → B
↓
Thread 2 changes B → A
↓
Thread 1 performs CAS expecting A
Thread 1 sees A again and assumes nothing changed.
But the value actually changed:
text
A → B → A
This can cause incorrect behavior in certain lock-free algorithms.
Possible solutions include:
* Version/tag counters
* Tagged pointers
* Hazard pointers
* Epoch-based reclamation
* Other safe memory-reclamation techniques
Interview takeaway: ABA is particularly important when implementing lock-free data structures involving pointer manipulation and memory reclamation.
───
17. What is the difference between notify_one() and notify_all()?
Given:
cpp
cv.notify_one();
Only one waiting thread is notified.
With:
cpp
cv.notify_all();
All waiting threads are notified.
Example:
text
Condition Variable
/ | \
Thread A Thread B Thread C
notify_one() → wakes one
notify_all() → wakes all
Use notify_one() when only one worker needs to proceed.
Use notify_all() when multiple waiting threads may need to re-check the condition.
Remember that being notified doesn't guarantee the thread will proceed—it must reacquire the mutex and re-check the predicate.
───
18. What happens if a std::thread is destroyed while still joinable?
This is a very common interview trap.
If a std::thread object is destroyed while joinable() == true, its destructor calls:
cpp
std::terminate();
Example:
cpp
void foo() {
std::thread t([] {
// work
});
} // std::terminate()!
You must either:
cpp
t.join();
or:
cpp
t.detach();
before destruction.
With C++20, std::jthread is safer because it automatically joins when destroyed.
───
19. What is std::shared_mutex and how is it different from std::mutex?
std::shared_mutex allows multiple readers or one writer.
text
Readers:
R1 ─┐
R2 ─┼── Can execute concurrently
R3 ─┘
Writer:
W ─── Exclusive access
Example:
cpp
std::shared_mutex mutex;
void read() {
std::shared_lock lock(mutex);
// Read shared data
}
void write() {
std::unique_lock lock(mutex);
// Modify shared data
}
With std::mutex, only one thread can hold the mutex regardless of whether it is reading or writing.
Use case: Data that is read frequently but modified relatively infrequently.
───
20. Explain a producer-consumer implementation using condition_variable.
This is one of the most important practical concurrency interview questions.
The producer adds work to a queue:
cpp
{
std::lock_guard<std::mutex> lock(mutex);
queue.push(item);
}
cv.notify_one();
The consumer waits until work is available:
cpp
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [] {
return !queue.empty();
});
auto item = queue.front();
queue.pop();
Conceptually:
text
Producer
|
| push()
v
+----------------+
| Shared Queue |
+----------------+
|
| notify
v
+----------------+
| Consumer |
| wait() |
+----------------+
The important points are:
1. The queue is protected by a mutex.
2. The consumer sleeps rather than continuously polling.
3. The condition variable wakes the consumer.
4. The consumer checks the condition again after waking.
5. The mutex protects both checking and modifying the queue.
───
For a senior C++ interview, I'd expect follow-up questions around memory ordering, atomics, lock-free data structures, cache coherence/false sharing, thread pools, and designing a thread-safe queue rather than just API definitions.
No comments:
Post a Comment