Skip to main content
Parallel Programming · lesson 1 of 8
Parallel Programming beginner Lesson 1 of 8

OpenMP 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

parallel-programming openmp c threads

#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.

Frequently Asked Questions

What does #pragma omp parallel actually do?
It spawns a team of threads and runs the block that follows on every one of them — not once, split across threads, but the entire block, once per thread. Dividing work across that team is a separate step (#pragma omp for), not something the parallel region does by itself.
Why did the loop run three times per thread instead of being split across threads?
A bare #pragma omp parallel only creates the team of threads; every thread independently executes everything inside the block, including a plain for loop with no work-sharing directive on it. #pragma omp for is what actually divides a loop's iterations across the current thread team — without it, "parallel" only means "duplicated," not "divided."
Why did omp_get_num_threads() return 1 outside a parallel region?
Outside any parallel region, there is only the single initial thread running — omp_get_num_threads() reports the size of the current thread team, and a team of one is exactly what's running at that point. omp_get_max_threads() answers a different question: how many threads would be used if a parallel region started right now, which is why it correctly reported the machine's real count.
Does OMP_NUM_THREADS always control how many threads a program uses?
Only when the code doesn't override it — an explicit num_threads(N) clause on #pragma omp parallel takes precedence over the environment variable for that specific region. OMP_NUM_THREADS sets the default; the clause is a per-region override.
Is the order threads print in predictable?
No, and it shouldn't be relied on — the OS scheduler decides which thread actually runs at any given instant, so "thread 0" finishing first is not guaranteed and will vary between runs, even with identical code and identical input. Code that depends on a specific thread finishing before another needs explicit synchronization, not luck.