Introduction
Welcome back! In our previous lesson, we explored the fundamental fork-join execution model of OpenMP. We learned how to create a team of threads using #pragma omp parallel, where each thread executed the same code block redundantly. While this was a great first step to understanding thread creation, the true power of parallel programming lies in dividing the work, not just duplicating it.
Today, we'll take that crucial next step. We will focus on one of the most common patterns in scientific and engineering computing: parallelizing loops. You will learn how to instruct OpenMP to automatically distribute the iterations of a for loop among the available threads. This is known as work-sharing.
This process, however, introduces a critical challenge: managing data. With multiple threads working on the same loop, we must be explicit about which variables are shared among all threads and which are private to each thread. Getting this wrong is a primary source of bugs in parallel programs.
By the end of this lesson, you will be able to parallelize loops using #pragma omp parallel for and manage the scope of shared and private variables.
1. Parallelizing Loops with Work-Sharing
In the previous lesson, every thread in the parallel region executed the entire block of code. To parallelize a loop, we don't want every thread to run every iteration. Instead, we want to split the iterations among the threads. For example, if we have 100 iterations and 4 threads, a simple strategy would be for thread 0 to handle iterations 0-24, thread 1 to handle 25-49, and so on.
OpenMP makes this remarkably simple with a work-sharing construct.
To learn about the syntax, let's start with a reading from a tutorial by Bowdoin College.
Intro to Parallel Programming with OpenMP
This resource provides a clear and concise explanation of how to parallelize loops in OpenMP, covering the essential directives.
Please read the section titled 'Parallelizing loops'. It introduces two key directives: #pragma omp for: This directive is placed inside an existing parallel region to distribute the work of the subsequent loop. #pragma omp parallel for: This is a convenient shortcut that combines the creation of a parallel region with the distribution of a loop's iterations. Focus on the code examples for vector addition.
As you saw, you can either nest a #pragma omp for inside a #pragma omp parallel block, or you can use the combined #pragma omp parallel for directive, which is more common for simple loop parallelization.
Now, let's see a demonstration. The following video implements a numerical integration example, but the key takeaway is observing how #pragma omp for is applied to a loop to distribute its work.
Parallel computing in C++: OpenMP
This video by Jonathon Riddell demonstrates the practical application of the #pragma omp for directive.
Watch the segment from 14:07 to 15:58. The core of this clip is seeing how the #pragma omp for directive is placed immediately before a for loop within a parallel region to achieve work-sharing.
2. Understanding Variable Scope: shared vs. private
Parallelizing the loop is only half the battle. Now that multiple threads are executing different iterations—potentially at the same time—we must consider how they interact with variables. This is the concept of data scope.
In OpenMP, variables used within a parallel region can be either:
- Shared: A single instance of the variable exists in memory, and all threads can read from and write to it. This is powerful but also dangerous, as it can lead to race conditions.
- Private: Each thread gets its own separate copy of the variable. Operations a thread performs on its private copy are invisible to other threads.
This short video from HPC Education provides a crisp definition of these two categories.
OpenMP: Shared and Private Variables
This video clearly defines the fundamental difference between shared and private variables in OpenMP.
Watch the brief segment from 00:08 to 00:37.
To visualize this, consider the following diagram. Before the parallel region, a single master thread has its own set of variables. When the team of threads is forked, shared variables maintain their single instance, while private variables are duplicated for each thread.

So, how does OpenMP decide which variables are shared and which are private? It follows a set of default rules:
- Variables declared outside the parallel region are
sharedby default. - Variables declared inside the parallel region (or a parallel loop) are
privateby default. - The loop iteration variable of a parallelized loop is always
private.
Relying on these defaults can be risky. To see why, let's watch another segment that demonstrates the chaos that can ensue when a variable that should be private is left as shared.
Parallel computing in C++: OpenMP
This segment clearly demonstrates the practical difference between shared and private scope and the problems caused by mismanaging them.
Watch the clip from 07:33 to 10:18. Pay attention to what happens when the variable x is implicitly shared versus when it is explicitly made private. This highlights why careful scope management is non-negotiable.
3. Explicitly Managing Data Scope
To avoid the pitfalls of default scoping, OpenMP provides clauses to let you explicitly define the scope of each variable. Your experience with C++ memory management and variable scope will be very useful here, as the principle is similar: being explicit prevents bugs.
The primary clauses are:
private(list_of_variables)shared(list_of_variables)default(none)
The default(none) clause is particularly important. It cancels all default scoping rules and forces you, the programmer, to explicitly declare the scope for every single variable used in the parallel region. While this may seem like extra work, it's a critical safety feature that makes you consciously think about data handling, drastically reducing the chance of race conditions.
This next video provides a comprehensive overview of these clauses and strongly advocates for using default(none).
OpenMP: Shared and Private Variables
This video from HPC Education covers the syntax for data-scoping clauses and explains the best practice of using default(none).
Watch from 00:37 to 04:43. This covers: OpenMP's default rules for data sharing. The syntax for the shared and private clauses. The purpose and benefit of using default(none) to enforce explicit scoping.
The syntax for using these clauses is straightforward. The image below shows code examples in both C and Fortran. Notice how private(i2) is added to the pragma to ensure each thread gets its own copy of the i2 variable.

4. Putting It All Together: A Complete Example
Let's solidify these concepts with a complete, correct example: parallel vector addition.
The task is simple: for two vectors a and b, compute a third vector c such that c[i] = a[i] + b[i].
Here is the serial C++ code:
#include <iostream>
#include <vector>
void add_vectors(std::vector<double>& c, const std::vector<double>& a, const std::vector<double>& b) {
int N = c.size();
for (int i = 0; i < N; ++i) {
c[i] = a[i] + b[i];
}
}
Now, let's parallelize this using our best practices.
- We'll use the
#pragma omp parallel fordirective to create threads and distribute the loop iterations in one go. - We'll add
default(none)to force ourselves to think about every variable. - We'll then specify the scope for each variable:
a,b,c,N, andi.
Let's analyze the variables:
a,b,c: These are the vectors. All threads need to access the data within them. Thread 0 needs to write toc[0], thread 1 toc[1], etc. Since each thread operates on a different index, there's no race condition on the elements. The vectors themselves must be shared.N: This is the size of the vectors. It's a read-only value. Every thread needs to know the loop bound. It should be shared.i: This is the loop counter. Each thread will be working on its own set of indices. Therefore, each thread needs its own private copy ofi. The loop iteration variable is automatically private, but withdefault(none), we must list it. It must be private.
Here is the correct parallel implementation:
#include <iostream>
#include <vector>
#include <omp.h> // Don't forget the OpenMP header
void parallel_add_vectors(std::vector<double>& c, const std::vector<double>& a, const std::vector<double>& b) {
const int N = c.size();
// N, a, b, and c are captured from the outer scope.
#pragma omp parallel for default(none) shared(a, b, c, N) private(i)
for (int i = 0; i < N; ++i) {
// Each thread executes this line for a different 'i'.
// Since 'i' is private, and each thread writes to a unique c[i],
// there is no race condition.
c[i] = a[i] + b[i];
}
}
This code correctly and safely parallelizes the vector addition. By explicitly defining the scope of each variable, we have created a program that is both parallel and predictable.
Conclusion
In this lesson, we moved from simple thread creation to practical work-sharing. We learned how to parallelize one of the most fundamental constructs in programming—the for loop—and how to manage the data that flows through it.
Key Takeaways:
- The
#pragma omp parallel fordirective is a powerful tool for distributing the iterations of a loop across a team of threads. - Variables in a parallel region are either
shared(one copy for all threads) orprivate(one copy per thread). - Mismanaging data scope is a primary cause of race conditions. Variables declared outside a parallel region are
sharedby default. - Using
default(none)is a crucial best practice. It forces you to explicitly define the scope of every variable with theshared()andprivate()clauses, preventing common parallel programming errors.
Preview of the Next Lesson
Our vector addition example worked smoothly because each thread wrote to a unique memory location (c[i]). But what if all threads need to contribute to a single, final value? For instance, what if we wanted to calculate the sum of all elements in an array? If we use a shared sum variable, we will immediately create a race condition.
In the next lesson, we will tackle this exact problem. You will learn about the reduction clause, an elegant and efficient OpenMP feature for safely performing aggregation operations like sums, products, and finding maximums in parallel.