If you have ever written a multi-threaded C program on Linux and seen it hang forever, or seen a counter come out wrong even though “the code looks right,” you have run into the exact problem this lecture solves. This lecture is part of our free Linux kernel development course and its companion free Linux device drivers course track on EmbeddedPathashala, and it covers the two building blocks every threaded program on Linux depends on: the mutex and the condition variable.
This is squarely aimed at students following our free embedded systems course path who already understand what a POSIX thread is and now need to make several of them cooperate safely.
What You Will Learn
- Why threaded programs need a special compiler flag
- How mutexes protect shared data
- The three classic mutex failure modes
- How condition variables let threads wait for a change of state
- A working producer/consumer style demo you can build and run today
Prerequisites
Before You Start
You should already be comfortable creating threads with pthread_create() and joining them with pthread_join(). You need a Linux machine with GCC and glibc — any current distribution (Ubuntu 24.04, Debian 12, Fedora 40, or newer) ships everything required.
Building a Threaded Program Correctly
On Linux, POSIX thread support lives inside glibc itself — modern distributions no longer ship a separate libpthread.so as a distinct linkable library the way older systems did, but the historical convention of asking the compiler for threading support still applies and is still required. It is not just about linking a library. The compiler has to change how it generates code so that per-thread state — the most obvious example being errno — gets one private copy per thread instead of a single copy shared by the whole process.
-lpthread. The -pthread flag tells both the compiler and the linker to enable thread-safety behavior; passing only the library name can silently produce a binary with subtly broken per-thread state.
gcc -Wall -pthread -o ep_sync_demo ep_sync_demo.c
Why Threads Need Synchronization
The entire appeal of threads, compared to separate processes, is that they share one address space. Any global or heap variable is visible to every thread in the process instantly, with no IPC mechanism required. That convenience is also the danger: if two threads read and modify the same variable without coordination, the result depends on the exact interleaving of machine instructions the scheduler happens to produce, and that interleaving is not something your program controls.
Threads can opt out of sharing for specific variables using thread-local storage (TLS), declared in modern GCC/Clang with the __thread or _Thread_local storage-class keyword. A TLS variable gets a private instance per thread automatically, which is often the simplest fix when a variable genuinely does not need to be shared.
#include <stdio.h>
#include <pthread.h>
/* One private copy of this counter per thread — no locking needed */
static _Thread_local int ep_local_counter = 0;
void *ep_worker(void *arg)
{
for (int i = 0; i < 5; i++)
ep_local_counter++;
printf("thread %p private counter = %d\n",
(void *)pthread_self(), ep_local_counter);
return NULL;
}
For data that genuinely must be shared, Linux gives pthreads two core synchronization primitives: the mutex, for mutual exclusion, and the condition variable, for signaling a change of state. It is worth remembering that the IPC mechanisms used between separate processes — pipes, message queues, shared memory with semaphores — also work between threads in the same process, since threads are just schedulable units inside one process. Mutexes and condition variables are simply the lighter-weight tools built specifically for the single-process, shared-memory case.
Mutual Exclusion With pthread_mutex_t
The rule for using a mutex correctly is simple to state and easy to violate in practice: every shared resource gets one mutex, and every code path that reads or writes that resource must lock the mutex first and unlock it afterward, with no exceptions.
#include <pthread.h>
static pthread_mutex_t ep_lock = PTHREAD_MUTEX_INITIALIZER;
static long ep_shared_total = 0;
void ep_add_to_total(long value)
{
pthread_mutex_lock(&ep_lock);
ep_shared_total += value;
pthread_mutex_unlock(&ep_lock);
}
Applying that one rule consistently solves most correctness problems, but mutexes have three well-known failure modes you should recognize by name even before you hit them in production.
| Failure Mode | What Happens | Common Mitigation |
|---|---|---|
| Deadlock | Two threads each hold one of two mutexes and wait forever for the other one | Always acquire multiple mutexes in the same global order; consider timed locks |
| Priority inversion | A high-priority thread blocks on a mutex held by a low-priority thread, which itself gets preempted by medium-priority threads | Priority-inheritance or priority-ceiling mutex protocols (see below) |
| Poor performance | Heavy contention on one mutex serializes work that could otherwise run in parallel | Finer-grained locking, or a different data structure/algorithm |
Linux’s pthreads implementation supports priority-inheritance mutexes to address the second problem directly. You opt in by setting the protocol on a mutex attribute object before creating the mutex:
pthread_mutexattr_t ep_attr;
pthread_mutexattr_init(&ep_attr);
pthread_mutexattr_setprotocol(&ep_attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_t ep_rt_lock;
pthread_mutex_init(&ep_rt_lock, &ep_attr);
Signaling State Changes With Condition Variables
A mutex alone only protects data — it does not tell a waiting thread when the data has become interesting. That is the job of a condition variable (pthread_cond_t). A condition is any boolean test on shared state — “is the queue empty,” “has the flag been set” — and the condition variable is the mechanism used to wake up threads that are waiting for that test to become true.
A condition variable is always paired with the mutex that protects the state being tested. The calling convention looks unusual the first time you see it: the waiting thread holds the mutex locked when it calls pthread_cond_wait(), and the function atomically unlocks the mutex while the thread sleeps, then re-locks it before returning — so there is no window where another thread could change the state between the check and the sleep.
Note the while loop around the wait call rather than a single if check. This guards against spurious wakeups, which POSIX explicitly permits, and against the case where two consumers both wake up but only one item was produced — re-checking the condition after waking is mandatory, not optional defensive style.
A Small Bounded Queue Example
Here is an original single-producer, single-consumer example built around a tiny fixed-size ring buffer, showing a mutex and condition variable working together.
#include <stdio.h>
#include <pthread.h>
#define EP_QUEUE_CAP 4
static int ep_queue[EP_QUEUE_CAP];
static int ep_head = 0, ep_tail = 0, ep_count = 0;
static pthread_mutex_t ep_qlock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t ep_qready = PTHREAD_COND_INITIALIZER;
void ep_queue_push(int value)
{
pthread_mutex_lock(&ep_qlock);
ep_queue[ep_tail] = value;
ep_tail = (ep_tail + 1) % EP_QUEUE_CAP;
ep_count++;
pthread_cond_signal(&ep_qready);
pthread_mutex_unlock(&ep_qlock);
}
int ep_queue_pop(void)
{
pthread_mutex_lock(&ep_qlock);
while (ep_count == 0)
pthread_cond_wait(&ep_qready, &ep_qlock);
int value = ep_queue[ep_head];
ep_head = (ep_head + 1) % EP_QUEUE_CAP;
ep_count--;
pthread_mutex_unlock(&ep_qlock);
return value;
}
void *ep_producer(void *arg)
{
for (int i = 1; i <= 8; i++) {
printf("producing %d\n", i);
ep_queue_push(i);
}
return NULL;
}
void *ep_consumer(void *arg)
{
for (int i = 0; i < 8; i++) {
int v = ep_queue_pop();
printf("consumed %d\n", v);
}
return NULL;
}
int main(void)
{
pthread_t prod, cons;
pthread_create(&cons, NULL, ep_consumer, NULL);
pthread_create(&prod, NULL, ep_producer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
return 0;
}
Build and run it:
$ gcc -Wall -pthread -o ep_sync_demo ep_sync_demo.c
$ ./ep_sync_demo
producing 1
producing 2
consumed 1
producing 3
consumed 2
producing 4
consumed 3
producing 5
consumed 4
producing 6
consumed 5
producing 7
consumed 6
producing 8
consumed 7
consumed 8
The exact interleaving of “producing” and “consumed” lines will vary between runs — that is expected, and is exactly why the mutex and condition variable are there: correctness does not depend on a particular ordering, only on the invariant that no item is read before it is written and none is lost.
Common Mistakes and Troubleshooting
if (ep_count == 0) instead of while around pthread_cond_wait() is the single most common condition-variable bug — it works fine in casual testing and fails unpredictably under load.
pthread_cond_signal(), while still holding the mutex (as shown above). Signaling before the state change can wake a thread that finds nothing new when it checks.
Best Practices
- Keep the critical section — the code between lock and unlock — as short as possible.
- Never call blocking I/O while holding a mutex unless you fully understand the contention impact on every other thread waiting for that lock.
- Prefer
pthread_cond_broadcast()overpthread_cond_signal()whenever more than one waiting thread might need to re-check the condition. - Use
PTHREAD_MUTEX_INITIALIZERfor statically allocated mutexes andpthread_mutex_init()for dynamically allocated ones — don’t mix the two patterns on the same object.
Performance Considerations
A mutex itself is cheap in the uncontended case — locking and unlocking an uncontended futex-backed pthread mutex costs roughly the price of an atomic instruction. The real cost shows up under contention, when many threads queue up for the same lock and the kernel has to actually put threads to sleep and wake them. If profiling shows a single mutex as a bottleneck, the fix is almost always finer-grained locking — splitting one big lock into several smaller ones that protect independent pieces of data — rather than trying to make the lock itself faster.
Summary and Key Takeaways
- Always compile and link threaded programs with
-pthread, not just-lpthread. - Every shared resource needs exactly one mutex, locked on every access path with no exceptions.
- Watch for deadlock, priority inversion, and lock contention as the three classic mutex problems.
- Condition variables let a thread sleep until shared state changes, without busy-waiting.
- Always re-check the condition in a
whileloop after waking frompthread_cond_wait().
Conclusion
Mutexes and condition variables are the foundation everything else in POSIX threading is built on — thread pools, work queues, and higher-level concurrency libraries all reduce to these two primitives underneath. Get comfortable with the lock/unlock discipline and the wait-in-a-loop pattern shown here, and you will have the tools to reason about almost any threaded design you encounter in embedded Linux work. This lecture is one part of EmbeddedPathashala’s ongoing free Linux kernel development course — the next lecture in this chapter builds directly on these primitives to talk about how to structure processes and threads at a system-design level.
Frequently Asked Questions
Do I still need to link libpthread.so separately on modern Linux?
No. On current glibc versions, pthread support is folded into the main C library, but you should still compile and link with -pthread so the compiler generates correct thread-local code and the linker pulls in the right runtime support.
What is the difference between pthread_cond_signal and pthread_cond_broadcast?
pthread_cond_signal() wakes at most one waiting thread. pthread_cond_broadcast() wakes all of them. Use broadcast whenever the change could satisfy more than one waiter, or when you are not certain only one waiter cares.
Can a spurious wakeup really happen with no signal at all?
Yes. POSIX explicitly allows pthread_cond_wait() to return without any corresponding signal or broadcast, which is exactly why the condition must always be re-checked in a loop rather than assumed true after waking.
Is a mutex the same thing as a semaphore?
No. A mutex is a binary lock owned by the thread that locked it, meant purely for mutual exclusion. A semaphore is a counter that any thread can increment or decrement and is better suited to signaling and resource counting than to protecting a critical section.
Why does pthread_cond_wait need the mutex passed in as an argument?
Because it has to atomically unlock that specific mutex as part of going to sleep, and re-lock the same mutex before returning. Passing the mutex explicitly is what lets the operation be atomic and avoids the race where a signal could be missed between checking the condition and starting to wait.
What happens if I forget to unlock a mutex?
Every other thread that tries to lock it blocks forever — a classic self-inflicted deadlock. Structuring code so every lock has one clearly matched unlock (or using scope-based cleanup patterns) avoids this class of bug.
Are priority-inheritance mutexes on by default?
No, you must explicitly request the PTHREAD_PRIO_INHERIT protocol through a mutex attribute object before creating the mutex. The default protocol does nothing to prevent priority inversion.
Continue the Free Linux Kernel Development Course
This lecture is part of EmbeddedPathashala’s free Linux device drivers and embedded Linux curriculum. Explore the rest of the Processes and Threads chapter and the full course index.
Browse the Full Course Next Lecture
2 Comments