What Most People Miss When Prepping for C Multithreading Interviews
The standard approach is to memorize definitions of mutexes, semaphores, and condition variables. That gets you through the first ten minutes of an interview, then the interviewer asks something that makes it clear you've never actually written concurrent code under load. The real test is whether you understand what goes wrong when two threads touch the same memory without synchronization, and more importantly, why simple solutions don't always work. I spent years debugging production systems where race conditions showed up maybe once every three weeks under specific timing conditions. The code would compile clean, pass every test suite, and then silently corrupt data in production. This is what multithreading in C actually feels like before you learn to respect it.
Multithreading Interview Questions C Every Candidate Should Actually Know
Before we get into the specific questions you should prepare for, here's a download link to a curated list I compiled from actual interviews at mid-to-large engineering companies. It covers everything from basic pthread creation to the trickier questions about memory ordering and cache coherency. Download it here: Multithreading Interview Questions C Guide (PDF). Let me start with something most people get backwards. The question isn't usually "what is a mutex?" The question they ask is "walk me through what happens when you try to lock a mutex that's already held." That tests whether you understand blocking, context switching, and the kernel's role. A mutex involves putting the current thread to sleep in the kernel, which triggers a context switch. The scheduler picks another runnable thread. When the mutex is unlocked, the kernel wakes your thread and it competes for CPU time again. That's the basic mechanism, and knowing it matters because it leads directly into deadlock discussions. Deadlock is probably the most common interview topic after basic synchronization. The four Coffman conditions must all hold for a deadlock to occur: mutual exclusion, hold and wait, no preemption, and circular wait. Interviewers want you to identify which condition you can break and how. Resource ordering is the standard answer. If you always acquire locks in a consistent order, circular wait becomes impossible. I once had an intern spend three days chasing a deadlock that came down to two functions acquiring mutexes in opposite orders. Breaking one of those lock order inversions fixed it immediately.
Now let me address something counter-intuitive. More locks does not mean more safety. In fact, adding locks to protect shared state often creates more problems than it solves. I once saw a codebase where someone added a separate mutex to every field in a struct to avoid contention. The result was worse performance and more subtle bugs because the invariants across fields were no longer protected atomically. The right answer is usually coarser-grained locking or lock-free data structures, depending on the use case. Here's another one that catches people off guard. Thread-local storage. The keyword is _Thread_local in C11, or __thread as a GCC extension that predates the standard. A common question is "when would you use TLS instead of a mutex?" The answer involves performance and reentrancy. If each thread maintains its own copy of a value, you eliminate contention entirely. I used this pattern for a per-thread buffer pool in a network server, and it cut allocation latency by roughly 60% compared to the mutex-protected version. But TLS has a cost. It increases memory usage linearly with thread count, and it doesn't help when threads need to coordinate or share state intentionally. Condition variables are where most candidates stumble. The standard pattern is:
Get the Full Details
pthread_mutex_lock(&mutex); while (!condition) { pthread_cond_wait(&cond, &mutex);
} pthread_mutex_unlock(&mutex); Note the while, not if. Spurious wakeups are a real thing on some systems, and even when they're not, other threads can change the condition between wakeup and re-acquisition of the lock. Using if means you might proceed with stale assumptions. I saw this cause a memory leak in a producer-consumer implementation where a spurious wakeup led to a thread reading from an empty queue and returning null unexpectedly.
Semaphores come up less frequently but when they do, the distinction between counting and binary semaphores matters. A binary semaphore is essentially a mutex without ownership. Any thread can signal it, which is useful for signaling events rather than protecting resources. A classic interview question is implementing a bounded buffer with semaphores. You need two: one counting available slots and one counting filled items. The producer decrements available slots and increments filled items. The consumer does the opposite. Priority inversion is an advanced topic that separates people who've read about real-time systems from people who've only done casual concurrency work. A low-priority thread holds a lock that a high-priority thread needs. A medium-priority thread preempts the low-priority one, causing the high-priority thread to wait indirectly for the medium one. The solution is priority inheritance: when a high-priority thread blocks on a lock held by a low-priority thread, the low-priority thread temporarily inherits the high priority. Linux implements this in its futex-based mutexes. Memory ordering is the deepest rabbit hole here. C11 introduced stdatomic.h with memory ordering constraints like memory_order_relaxed, memory_order_acquire, memory_order_release, and memory_order_seq_cst. The default is sequential consistency, which is correct but expensive on architectures like ARM that have weaker memory models. A common interview question asks what happens when you use memory_order_release on one thread and memory_order_acquire on another. The answer involves happens-before relationships. The release operation synchronizes-with the acquire operation, establishing a partial order that prevents the compiler and CPU from reordering certain operations across the boundary.

I spent a week in 2019 debugging a crash that turned out to be a missing memory barrier in a lock-free queue. The compiler was reordering the write to the data element with the write to the next pointer. Another thread would see the updated next pointer, follow it, and read garbage data because the actual payload hadn't been written yet. The fix was adding the appropriate acquire-release semantics. This is the kind of thing that doesn't show up in any textbook example but comes up in senior-level interviews. Thread cleanup handlers are another practical detail. pthread_cleanup_push and pthread_cleanup_pop register functions that run when a thread exits, whether normally or due to cancellation. They're essential for releasing resources in the right order. I once wrote a threaded logger where each thread opened a file handle, and without cleanup handlers, cancelled threads would leak file descriptors. The operating system would eventually run out and the whole program would fail. Joining versus detaching threads is straightforward but important. pthread_join blocks until the target thread terminates and also reaps its resources. pthread_detach makes the thread self-reaping but you lose the ability to wait for it. If you detach a thread and the main program exits, the detached thread may be terminated prematurely, losing work. This is a common bug in daemon-style applications where threads are detached for fire-and-forget tasks.
Futexes are Linux-specific but frequently asked about in systems programming roles. A futex is a fast userspace lock that avoids kernel involvement when there's no contention. The basic operation is a compare-and-swap in userspace. If the lock is free, you acquire it without a syscall. If it's contended, you fall back to the kernel. This is what modern libraries like glibc's mutex implementation use under the hood. Understanding futexes shows you know how the abstraction is built, not just how to call it. The C11 threading API (<threads.h>) is another topic that separates the curious from the rest. It provides thrd_create, thrd_join, mtx_lock, and cnd_signal, among others. It's standardized across compilers and platforms, unlike pthreads which is POSIX. The downside is that it's less widely used in practice, so some interviewers won't expect it. But mentioning you know both APIs demonstrates breadth. Barrier synchronization comes up in parallel algorithm questions. pthread_barrier_wait blocks threads until a specified count has reached the barrier, then releases them all simultaneously. This is useful for pipeline stages where each stage must complete before the next begins. I used barriers in a parallel image processing pipeline where each stage (resize, filter, enhance) had to finish processing all pixels before the next stage started. Without barriers, later stages would read partially computed data.
One limitation worth being honest about: C's threading model doesn't include garbage collection or safe thread suspension. You manage your own memory, and a thread can be cancelled at almost any point. This means any code path that acquires resources must also have a corresponding release path, usually via cleanup handlers or careful error handling. This is a fundamental difference from languages like Java where the runtime provides more safety guarantees. Another limitation is that POSIX threads don't provide any built-in debugging support for race conditions. You need external tools like ThreadSanitizer (-fsanitize=thread) to detect data races at runtime. I recommend running your test suite with TSan during development. It catches races that static analysis misses and will find issues your code review never would. For interview preparation, focus on these areas: the actual mechanics of mutex and condition variable usage, deadlock prevention strategies, the difference between process and thread synchronization primitives, and the C11 atomic operations library. The questions that matter most are the ones where they give you a snippet of code and ask what's wrong with it. Practice finding race conditions, potential deadlocks, and missed optimizations in sample code.

Download the full question list and solutions here: Multithreading Interview Questions C Complete Guide.