In Module 8, each worker wrote a different partial[id] slot. After joining the workers, the main thread combined their results. Now we will see what changes when several workers update the same variable.
This follows Chapter 4’s shared-sum problem (§4.4) and mutex solution (§4.6). The book estimates pi; we will start with a counter so we can concentrate on the shared update. The three complete programs are:
- pth_counter_race.c — An intentionally incorrect shared counter.
- pth_counter_mutex.c — Protect each increment with a mutex.
- pth_counter_local.c — Count locally, then protect one update per worker.
Try an incorrect counter
Create pth_counter_race.c:
#include <stdio.h>
#include <pthread.h>
#define THREAD_COUNT 4
#define REPEATS 100000
long long total = 0;
void *count(void *argument) {
(void)argument;
for (int i = 0; i < REPEATS; i++) total++;
return NULL;
}
int main(void) {
pthread_t workers[THREAD_COUNT];
for (int t = 0; t < THREAD_COUNT; t++) {
pthread_create(&workers[t], NULL, count, NULL);
}
for (int t = 0; t < THREAD_COUNT; t++) {
pthread_join(workers[t], NULL);
}
printf("Expected: %lld\n", (long long)THREAD_COUNT * REPEATS);
printf("Observed: %lld\n", total);
return 0;
}Each worker does the same amount of work, so it does not need an ID. The last NULL in pthread_create supplies no argument. (void)argument tells the compiler that we intentionally do not use the parameter. It does not dereference a pointer or move any data.
gcc -Wall -O0 -pthread -o pth_counter_race pth_counter_race.c
./pth_counter_race
./pth_counter_race
./pth_counter_raceThe intended result is 400,000, but this program has a data race: workers access the same memory without synchronization, and those accesses include writes. In C that is undefined behavior, so we cannot rely on the result. You might see lost updates, different results between runs, or even 400,000. A correct-looking run does not make the program correct. -O0 makes this a convenient demonstration; it does not repair the race. Do not use this incorrect version as a performance baseline.
The join loop makes the main thread wait for completion. It does not coordinate the workers’ increments while they run. Changing the spelling of the increment would not help either.
Protect the critical section
A critical section accesses shared data that needs protection. A mutex is a lock: only one worker can hold this lock at a time. Every worker that updates total must use the same mutex.
Copy the program:
cp pth_counter_race.c pth_counter_mutex.cAdd this shared declaration beside total:
pthread_mutex_t total_lock;In main, initialize the mutex before creating any workers:
pthread_mutex_init(&total_lock, NULL);Replace the worker’s loop with:
for (int i = 0; i < REPEATS; i++) {
pthread_mutex_lock(&total_lock);
total++;
pthread_mutex_unlock(&total_lock);
}After joining all workers, destroy the mutex before returning from main:
pthread_mutex_destroy(&total_lock);The & passes the mutex’s address to these functions. This is the same address pattern we already used for IDs and thread handles. The second argument to initialization is NULL to request the default mutex attributes.
gcc -Wall -O0 -pthread -o pth_counter_mutex pth_counter_mutex.c
./pth_counter_mutexThe expected and observed values should now both be 400,000. As in our first thread examples, these teaching programs assume successful thread and mutex calls.
Discuss: Why must the lock cover the whole increment, rather than just the final write? What happens if a worker takes the lock and never unlocks it? Would a different mutex for each worker protect this shared total?
The mutex protects access to total; it does not choose which worker goes first. If a worker waits for a lock, it cannot enter that critical section until it acquires the lock. All workers must follow the same locking rule.
Do most of the work locally
The previous version takes a lock 400,000 times. Only one worker at a time can update the counter, so the updates are effectively serialized. We can do better by keeping a local count and locking just once at the end.
Copy the mutex version to pth_counter_local.c. Keep the shared declarations, initialization, creation, joins, and destruction. Replace the worker function with:
void *count(void *argument) {
(void)argument;
long long local = 0;
for (int i = 0; i < REPEATS; i++) local++;
pthread_mutex_lock(&total_lock);
total += local;
pthread_mutex_unlock(&total_lock);
return NULL;
}Each worker’s local belongs to its own function call. The loop needs no lock. The shared total += local still needs protection, because several workers may reach that statement together.
gcc -Wall -O0 -pthread -o pth_counter_local pth_counter_local.c
./pth_counter_localThe answer is still 400,000, but there are now four lock acquisitions. This is the same improvement used by the book’s mutex-based pi program: calculate a local contribution, then protect the shared addition. For this simple counter, an optimizing compiler may simplify the local loop, so we will use it to understand synchronization rather than as a realistic performance benchmark.
| Approach | Who writes the combined result? | How is it safe? |
|---|---|---|
| Module 8’s partial array | Main, after the joins | Workers use distinct slots; main waits before reading each result. |
| One lock per increment | All workers | Every increment uses the same mutex. |
| Local count, one locked addition | All workers | Local work is private; shared additions use the same mutex. |
Check your understanding
- Why does
pthread_joinnot repair the original race? - Which variables in the local-count program need protection, and which do not?
- Where do initialization and destruction belong in the thread lifecycle?
- If workers filled distinct output-array elements, would every write need a lock?
- Joining waits for completion; the workers can still overlap their updates before finishing.
- Concurrent updates to
totalneed the common mutex. A worker’s local counter does not. Main readstotalafter all joins, when workers no longer update it. - Initialize before launching workers; destroy after they have all finished using the mutex.
- Distinct elements can be written without a lock when no other thread concurrently accesses those same elements in a conflicting way. Main waits for the workers before reading their output, as in Module 8.