Linux System Programming · intermediate · ~10 min

Common mistakes with threads

Learn to recognize the threading bugs that trip up almost every C programmer when they first use pthreads.

Lesson

Here are eight mistakes that show up again and again in threaded C programs. Each one has a clear cause and a clear fix.

1. Forgetting -pthread

Leave this flag off and you get a linker error — or worse. On some platforms the code compiles, but pthread_mutex calls silently turn into no-ops, so your locking does nothing.

Always compile and link with -pthread.

2. Passing &i from a loop

If you start threads inside a loop and pass the address of the loop variable i, every thread ends up reading the same memory. By the time the threads run, i already holds its post-loop value, so they all see it.

Pass the value itself, or pass a pointer to memory allocated on the heap for each thread.

3. Race on a global counter

counter++ looks like one step, but it is really three: read, add, write. Two threads can interleave these steps and lose updates. This is a data race.

Protect the counter with a mutex, or declare it as _Atomic long.

4. Holding a mutex across a blocking I/O call

If a thread keeps a lock held while it waits on slow I/O, every other thread that needs that lock stalls too.

Release the lock before doing I/O.

5. Locking in different orders

If one path locks A then B, and another locks B then A, the two will eventually collide and wait on each other forever. This is a deadlock.

Adopt a single global rule for the order in which locks are acquired, and follow it everywhere.

6. Using signal() in a threaded program

The old signal() function has poorly defined behavior with threads.

Use sigaction() instead. Consider blocking signals on worker threads with pthread_sigmask.

7. Using if instead of while around cond_wait

A condition variable can wake a thread even when nothing changed — a spurious wakeup. If you check the condition with if, the thread continues on a false assumption.

Always re-check the condition in a while loop.

8. Returning a stack address from the thread function

The thread's stack disappears the moment the thread exits. Any pointer into it becomes invalid.

Return a heap pointer or a value, never the address of a local variable.

Code examples

/* WRONG — race on shared counter */
static int counter = 0;
void *bump(void *_) { for (int i = 0; i < 1000000; i++) counter++; return NULL; }

/* RIGHT — atomic */
static _Atomic int counter = 0;
void *bump(void *_) { for (int i = 0; i < 1000000; i++) counter++; return NULL; }

Common mistakes

  • Believing ThreadSanitizer is overkill. It is not. Run every multi-threaded C program under -fsanitize=thread (TSan) before shipping. It catches data races that testing alone will miss.

Summary

  • Eight recurring threading bugs to watch for.
  • Always compile with -pthread, and pass thread arguments safely (by value or by heap pointer).
  • Lock around shared state, and always use while (not if) around cond_wait.
  • Never return the address of a stack variable from a thread function.
  • Run your program under ThreadSanitizer (-fsanitize=thread) before shipping.

Practice with these exercises