linux-sysprog · intermediate · ~10 min

Fix the race with a pthread_mutex_t

Wrap the read-modify-write of a shared variable in `pthread_mutex_lock` / `pthread_mutex_unlock`.

Challenge

Cure the race from the last exercise

You just watched two threads lose updates to a shared counter (in demonstrate-race). The cure is one of the oldest tools in concurrent programming: a mutex. Acquire it before touching the shared variable, release it after.

Real-world frame

Every C server with shared state — a request counter, a stats table, an in-memory cache — has this exact pattern at the bottom of its critical sections. Get it right once and you never lose an update again.

Task

Implement:

long safe_counter(int nthreads, int per_thread);

Same shape as unsafe_counter, but each counter++ happens inside pthread_mutex_lock(&m); ...; pthread_mutex_unlock(&m);. The result must be exactly nthreads * per_thread every single time.

Function signature

long safe_counter(int nthreads, int per_thread);

Rules

  • Use pthread_mutex_t, not _Atomic (we want to practice the explicit lock).
  • The lock must be held for the entire read-modify-write of the counter — not just one of the steps.
  • Reset the counter to 0 at the start of every call so the harness can re-test.

Examples

nthreads per_thread returns
1 1000 1000
4 10000 40000 (exact, every time)
8 5000 40000 (exact)

Edge cases

  • nthreads <= 0 or > 64 → return -1.
  • A short loop count won't expose the original race, but the mutex makes the result deterministic regardless.

Hints

  1. Conceptual: mutex turns one race-prone step (counter++) into a critical section. Inside, exactly one thread executes at a time.
  2. Implementation: declare static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;. In the worker loop: pthread_mutex_lock(&m); g_counter++; pthread_mutex_unlock(&m);.
  3. Common bug: forgetting the unlock on an error path. Use a single exit point or goto cleanup; to make the unlock impossible to miss.

Common mistakes

  • Holding the lock across printf or sleep — fine here (tiny critical section), but in real code it stalls every other thread.
  • Locking inside the loop is correct but slow. For a real high-throughput counter, switch to _Atomic long for lock-free atomic add.

Learning connection

This is the cure for the bug you saw in demonstrate-race. With the lock in place, the result is deterministic and the program is correct.

Why this matters

Once you've felt the bug, the cure is satisfying. Lock-modify-unlock makes the program correct without any rewriting of the logic.

Input format

Two integers: nthreads, per_thread.

Output format

One long — exactly nthreads * per_thread.

Constraints

Use pthread_mutex_t. The lock must wrap the increment, not just the read or the write.

Starter code

#include <pthread.h>
long safe_counter(int nthreads, int per_thread) {
    /* TODO: same as unsafe_counter, but protect g_counter with a mutex. */
    return -1;
}

Common mistakes

Locking around the read but not the write. Forgetting pthread_mutex_unlock on an early return. Holding the mutex across I/O.

Edge cases to handle

Single thread: counter equals per_thread (no race possible). Many threads: counter equals nthreads * per_thread every time.

Background lessons

Up next

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.