Parallel Programming · lesson 3 of 8
Beginner
2Intermediate
5- 3 Race Conditions: A Sum That Gives Three Different Wrong Answers
- 4 Atomics vs Locks, and the Race Hiding Inside a Check-Then-Act
- 5 MPI Basics: a Real Deadlock That Only Shows Up at Scale
- 6 MPI Collectives: Why MPI_Reduce Leaves Every Rank but One Empty
- 7 OpenMP vs MPI, Combined: a Hybrid Program and a Real Oversubscription Penalty
Also part of Parallel Programming
2Race Conditions: A Sum That Gives Three Different Wrong Answers
Three runs of the same unsynchronized parallel sum produce three different wrong totals — reduction(+:sum) fixes it correctly and is 115x faster than the naive #pragma omp critical alternative, measured.
By SkillByExample · Updated
The same program, run three times, produces three different wrong answers — and the fix is also the fast version, not a slower-but-safer tradeoff.
The bug: a race condition with a different wrong answer every run
// race_sum.c
#include <stdio.h>
#include <omp.h>
int main() {
long sum = 0;
#pragma omp parallel for num_threads(8)
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
printf("sum = %ld (expected 500000500000)\n", sum);
return 0;
}
gcc -fopenmp -o race_sum race_sum.c
./race_sum
./race_sum
./race_sum
sum = 55969261013 (expected 500000500000)
sum = 197355475976 (expected 500000500000)
sum = 84382115934 (expected 500000500000)
Three runs, three different wrong totals, none of them correct. sum += i is really three
separate steps — read sum, add i, write the result back — and eight threads doing this
concurrently on the same shared variable interleave those steps unpredictably. One thread’s
write can silently overwrite another’s, losing that addition entirely. Which additions get
lost depends on OS thread scheduling, which is why the specific wrong answer changes every
time.
The fix: reduction(+:sum)
// fixed_sum.c
#include <stdio.h>
#include <omp.h>
int main() {
long sum = 0;
#pragma omp parallel for num_threads(8) reduction(+:sum)
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
printf("sum = %ld (expected 500000500000)\n", sum);
return 0;
}
gcc -fopenmp -o fixed_sum fixed_sum.c
./fixed_sum
./fixed_sum
./fixed_sum
sum = 500000500000 (expected 500000500000)
sum = 500000500000 (expected 500000500000)
sum = 500000500000 (expected 500000500000)
Correct, every time. reduction(+:sum) gives each thread its own private copy of sum to
accumulate into — no other thread can touch it, so there’s nothing to race over — and combines
all eight private totals into the final answer in one step at the end.
Measuring the alternative: #pragma omp critical
#pragma omp critical also fixes the race — by forcing every += through a lock instead of
giving each thread a private copy:
// critical_sum.c
#pragma omp parallel for num_threads(8)
for (int i = 1; i <= 1000000; i++) {
#pragma omp critical
sum += i;
}
gcc -fopenmp -o critical_sum critical_sum.c
./critical_sum
sum = 500000500000, time = 0.115 s
gcc -fopenmp -o timed_reduction timed_reduction.c # same as fixed_sum.c, with timing added
./timed_reduction
sum = 500000500000, time = 0.001 s
Both correct — 0.115s versus 0.001s, a measured 115x difference. critical forces a
million lock/unlock round trips, one per iteration, each one blocking every other thread that
arrives while it’s held. reduction needs zero locking during the loop at all; the only
coordination is combining eight private sums together once, at the very end.
Practice
1. Use a different reduction operator — find the maximum value in a loop.
int best = 0;
#pragma omp parallel for reduction(max:best)
for (int i = 0; i < 1000; i++) {
int candidate = (i * 37) % 997;
if (candidate > best) best = candidate;
}
printf("max = %d\n", best);
max = 996
reduction(max:best) works exactly like reduction(+:sum) — private per-thread copies, each
tracking its own maximum, combined into the overall maximum at the end. Same pattern, same
correctness guarantee, different combining operator.
2. Confirm the race condition reproduces with a smaller thread count too, just less severely.
OMP_NUM_THREADS=2 ./race_sum
sum = 107477038877 (expected 500000500000)
Still wrong — the race exists with as few as 2 threads racing on the same shared variable. Fewer threads means fewer possible interleavings and sometimes a result closer to correct by chance, but “closer” is not “correct,” and the bug is present regardless of thread count.
3. Verify reduction stays correct even at a much smaller thread count.
OMP_NUM_THREADS=2 ./fixed_sum
sum = 500000500000 (expected 500000500000)
Correct regardless of thread count, because reduction’s correctness comes from giving each
thread its own private accumulator — that guarantee doesn’t depend on how many threads there
happen to be.
Next: synchronization primitives — critical sections, atomics, and locks, and when each is the right tool.