Parallel Programming · lesson 1 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
2OpenMP Basics: Parallel Regions and the Loop That Ran on Every Thread
A bare #pragma omp parallel runs the whole block on every thread — including the entire loop inside it, redundantly, unless #pragma omp for actually divides the iterations.
By SkillByExample · Updated
#pragma omp parallel creates a team of threads — it does not, by itself, divide any work
between them. That distinction is where the first real bug in this lesson comes from.
A team of threads, each running the same block
// hello_omp.c
#include <stdio.h>
#include <omp.h>
int main() {
#pragma omp parallel
{
int id = omp_get_thread_num();
int total = omp_get_num_threads();
printf("Hello from thread %d of %d\n", id, total);
}
return 0;
}
gcc -fopenmp -o hello_omp hello_omp.c
./hello_omp
Hello from thread 0 of 12
Hello from thread 9 of 12
Hello from thread 11 of 12
Hello from thread 7 of 12
Hello from thread 8 of 12
Hello from thread 10 of 12
Hello from thread 6 of 12
Hello from thread 4 of 12
Hello from thread 3 of 12
Hello from thread 1 of 12
Hello from thread 2 of 12
Hello from thread 5 of 12
Twelve threads (this machine’s core count), each running the entire block once, printing in whatever order the OS scheduler happens to run them — not sorted by thread number, and not reproducible run to run.
OMP_NUM_THREADS=4 ./hello_omp
Hello from thread 0 of 4
Hello from thread 1 of 4
Hello from thread 3 of 4
Hello from thread 2 of 4
OMP_NUM_THREADS controls the team size without recompiling anything.
The bug: a loop that ran on every thread, not divided between them
// bug_loop.c
#include <stdio.h>
#include <omp.h>
int main() {
#pragma omp parallel num_threads(4)
{
for (int i = 0; i < 3; i++) {
printf("thread %d processing item %d\n", omp_get_thread_num(), i);
}
}
return 0;
}
gcc -fopenmp -o bug_loop bug_loop.c
./bug_loop
thread 0 processing item 0
thread 0 processing item 1
thread 0 processing item 2
thread 2 processing item 0
thread 2 processing item 1
thread 2 processing item 2
thread 3 processing item 0
thread 3 processing item 1
thread 3 processing item 2
thread 1 processing item 0
thread 1 processing item 1
thread 1 processing item 2
Twelve lines for a 3-item loop with 4 threads — every single thread ran the entire loop,
processing items 0, 1, and 2 all by itself. #pragma omp parallel only created the team; it
never told OpenMP to split the loop’s iterations across it, so each thread just ran everything
independently, redundantly.
The fix: #pragma omp for actually divides the work
// fixed_loop.c
#include <stdio.h>
#include <omp.h>
int main() {
#pragma omp parallel num_threads(4)
{
#pragma omp for
for (int i = 0; i < 3; i++) {
printf("thread %d processing item %d\n", omp_get_thread_num(), i);
}
}
return 0;
}
gcc -fopenmp -o fixed_loop fixed_loop.c
./fixed_loop
thread 0 processing item 0
thread 1 processing item 1
thread 2 processing item 2
Three lines this time — each of the three items processed exactly once, by a different
thread. #pragma omp for, placed inside the parallel region, is the directive that actually
distributes loop iterations across the current thread team instead of duplicating the whole
loop on each one.
Practice
1. Compare omp_get_max_threads() to omp_get_num_threads() outside any parallel region.
#include <stdio.h>
#include <omp.h>
int main() {
printf("max threads available: %d\n", omp_get_max_threads());
printf("num threads outside parallel region: %d\n", omp_get_num_threads());
return 0;
}
max threads available: 12
num threads outside parallel region: 1
omp_get_num_threads() reports the size of whatever thread team is currently running — outside
a parallel region, that’s just the one initial thread. omp_get_max_threads() answers a
different question: how many threads a parallel region would use if one started right now.
2. Confirm that an explicit num_threads() clause overrides OMP_NUM_THREADS.
OMP_NUM_THREADS=2 ./fixed_loop
thread 1 processing item 1
thread 2 processing item 2
thread 0 processing item 0
Still using up to 4 threads (only 3 are needed for 3 iterations) despite OMP_NUM_THREADS=2 —
num_threads(4) in the source code is a per-region override that wins over the environment
variable’s default.
3. Try to reduce the duplication by setting OMP_NUM_THREADS=2 on the buggy version, and see why it doesn't help.
OMP_NUM_THREADS=2 ./bug_loop
thread 0 processing item 0
thread 0 processing item 1
thread 0 processing item 2
thread 2 processing item 0
thread 2 processing item 1
thread 2 processing item 2
thread 3 processing item 0
thread 3 processing item 1
thread 3 processing item 2
thread 1 processing item 0
thread 1 processing item 1
thread 1 processing item 2
Still twelve lines, still 4 threads — bug_loop.c hardcodes num_threads(4) in its pragma,
which overrides OMP_NUM_THREADS exactly as confirmed in the previous task. This is also the
real lesson underneath the exercise: the redundant-loop bug and the thread-count-override
behavior are independent of each other. Fixing one doesn’t touch the other — the loop needs
#pragma omp for, full stop, regardless of how many threads are running it.
Next: dividing work with scheduling strategies, and what changes when iterations take unequal time.