Wednesday, October 7, 2026

C++ Threading interview questions

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